<commit_before>'use strict';

/** @module */

/**
 * Returns true if obj is a defined value
 * @param {Object} obj - the value to test
 * @returns {boolean}
 */
function defined (obj) {
    return typeof obj !== 'undefined';
}

/**
 * Returns true if obj is a non-null object
 * Checking for typeof 'object' without checking for nulls
 * is a very common source of bugs
 * @param {Object} obj - the value to test
 * @returns {boolean}
 */
function isObject (obj) {
    return typeof obj === 'object' && obj !== null;
}

/**
 * Returns the text used for logging purposes related to this socket
 * @param {Object} socket - the socket
 * @returns {string}
 */
function socketName (socket) {
    let result = socket.remoteAddress;
    if (socket.remotePort) {
        result += `:${socket.remotePort}`;
    }
    return result;
}

/**
 * Returns a deep clone of obj
 * @param {Object} obj - the object to clone
 * @returns {Object}
 */
function clone (obj) {
    return JSON.parse(JSON.stringify(obj));
}

/**
 * Returns a new object combining the two parameters
 * @param {Object} defaults - The base object
 * @param {Object} overrides - The object to merge from.  Where the same property exists in both defaults
 * and overrides, the values for overrides will be used
 * @returns {Object}
 */
function merge (defaults, overrides) {
    const result = clone(defaults);
    Object.keys(overrides).forEach(key => {
        if (typeof overrides[key] === 'object' && overrides[key] !== null) {
            result[key] = merge(result[key] || {}, overrides[key]);
        }
        else {
            result[key] = overrides[key];
        }
    });
    return result;
}

/**
 * Sets a value of nested key string descriptor inside a Object.
 * It changes the passed object.
 * Ex:
 *    let obj = {a: {b:{c:'initial'}}}
 *    setNestedKey(obj, ['a', 'b', 'c'], 'changed-value')
 *    assert(obj === {a: {b:{c:'changed-value'}}})
 *
 * @param {Object} obj   Object to set the nested key
 * @param {Array} path  An array to describe the path(Ex: ['a', 'b', 'c'])
 * @param {Object} value Any value
 * @returns {undefined}
 * from https://stackoverflow.com/a/49754647
 */
function setDeep (obj, path, value) {
    if (path.length === 1) {
        obj[path] = value;
        return;
    }
    setDeep(obj[path[0]], path.slice(1), value);
}

module.exports = { defined, isObject, socketName, clone, merge, setDeep };
<commit_msg>add objFilter<commit_after>'use strict';

/** @module */

/**
 * Returns true if obj is a defined value
 * @param {Object} obj - the value to test
 * @returns {boolean}
 */
function defined (obj) {
    return typeof obj !== 'undefined';
}

/**
 * Returns true if obj is a non-null object
 * Checking for typeof 'object' without checking for nulls
 * is a very common source of bugs
 * @param {Object} obj - the value to test
 * @returns {boolean}
 */
function isObject (obj) {
    return typeof obj === 'object' && obj !== null;
}

/**
 * Returns the text used for logging purposes related to this socket
 * @param {Object} socket - the socket
 * @returns {string}
 */
function socketName (socket) {
    let result = socket.remoteAddress;
    if (socket.remotePort) {
        result += `:${socket.remotePort}`;
    }
    return result;
}

/**
 * Returns a deep clone of obj
 * @param {Object} obj - the object to clone
 * @returns {Object}
 */
function clone (obj) {
    return JSON.parse(JSON.stringify(obj));
}

/**
 * Returns a new object combining the two parameters
 * @param {Object} defaults - The base object
 * @param {Object} overrides - The object to merge from.  Where the same property exists in both defaults
 * and overrides, the values for overrides will be used
 * @returns {Object}
 */
function merge (defaults, overrides) {
    const result = clone(defaults);
    Object.keys(overrides).forEach(key => {
        if (typeof overrides[key] === 'object' && overrides[key] !== null) {
            result[key] = merge(result[key] || {}, overrides[key]);
        }
        else {
            result[key] = overrides[key];
        }
    });
    return result;
}

/**
 * Sets a value of nested key string descriptor inside a Object.
 * It changes the passed object.
 * Ex:
 *    let obj = {a: {b:{c:'initial'}}}
 *    setNestedKey(obj, ['a', 'b', 'c'], 'changed-value')
 *    assert(obj === {a: {b:{c:'changed-value'}}})
 *
 * @param {Object} obj   Object to set the nested key
 * @param {Array} path  An array to describe the path(Ex: ['a', 'b', 'c'])
 * @param {Object} value Any value
 * @returns {undefined}
 * from https://stackoverflow.com/a/49754647
 */
function setDeep (obj, path, value) {
    if (path.length === 1) {
        obj[path] = value;
        return;
    }
    setDeep(obj[path[0]], path.slice(1), value);
}

/**
 * Remove specific key and value from object
 * @param {Object} obj Object to filter
 * @param {Array|Object|String} filter keys to remove
 * @returns {Object}
 */

function objFilter (obj, filter) {

    if (typeof filter === 'string') {
        delete obj[filter];
    }
    else if (Array.isArray(filter)) {
        filter.filter(keyFilter => obj[keyFilter]).forEach(keyFilter => objFilter(obj, keyFilter));
    }
    else {
        Object.keys(filter).filter(keyFilter => obj[keyFilter]).forEach(keyFilter => objFilter(obj[keyFilter], filter[keyFilter]));
    }
    return obj;
}

module.exports = { defined, isObject, socketName, clone, merge, setDeep, objFilter };
<|endoftext|>


<commit_before>import Em from 'ember';

export default Em.Component.extend({

  tagName: 'bm-select',

  classNameBindings: ['isOpen'],

  attributeBindings: [
    'role',
    'ariaMultiselectable:aria-multiselectable',
    'ariaExpanded:aria-expanded',
    'tabindex',
    'value'
  ],

  /**
   * This flag is set to true/false based on whether the dropdown is
   * opened/closed.
   *
   * @property isOpen
   * @type Boolean
   */
  isOpen: false,

  /**
   * See http://www.w3.org/TR/wai-aria/roles#listbox
   *
   * @property role
   * @type String
   * @private
   */
  role: 'listbox',

  /**
   * Tells screenreaders whether this widget allows multiple selection.
   *
   * @property 'ariaMultiselectable'
   * @type String
   * @private
   */
  ariaMultiselectable: 'false',

  /**
   * Tells screenreaders whether this widget can be expanded.
   *
   * @property 'ariaExpanded'
   * @type String
   * @private
   */
  ariaExpanded: function() {
    return this.get('isOpen')+'';
  }.property('isOpen'),

  /**
   * Allows the component to get focus on tab press.
   *
   * @property tabindex
   * @type Number
   */
  tabindex: 0,

  /**
   * The value of the selected option. Must be a primitive value.
   *
   * @property value
   */
  value: null,

  /**
   * Reference to the BmOptionsComponent instance.
   *
   * @property options
   * @type BmOptions
   */
  options: null,

  /**
   * Reference to the selected BmOptionComponent instance.
   *
   * @property selectedOption
   * @type BmOption
   */
  selectedOption: null,

  /**
   * Register the BmOptionsComponent instance.
   *
   * @method registerOptions
   */
  registerOptions: function(options) {
    this.set('options', options);
  },

  /**
   * Select an option.
   *
   * @method selectOption
   */
  selectOption: function(item) {
    this.set('selectedOption', item);
    this.set('value', item.get('value'));
    this.sendAction('action', item.get('data'), item.get('value'));
  },

  /**
   * Toggles the dropdown, bound to click.
   *
   * @method toggleOptions
   */
  toggleOptions: function() {
    if(this.get('isOpen')) {
      this.closeOptions();
    } else {
      this.openOptions();
    }
  }.on('click'),

  /**
   * Opens the dropdown.
   *
   * @method openOptions
   */
  openOptions: function() {
    this.set('isOpen', true);
    this.get('options').focusItemAt(this.get('options.selectedIndex'));
  },

  /**
   * Closes the dropdown.
   *
   * @method closeOptions
   */
  closeOptions: function(options) {
    options = options || { focus: true};

    this.set('isOpen', false);
    this.set('options.focusIndex', -1);

    //set focus back to bm-select on dropdown close.
    //dont do this if focussing out or click outside.
    if(options.focus) {
      Em.run.schedule('afterRender', this, function() {
        this.$().focus();
      });
    }
  },

  /**
   * Closes the dropdown when focus changes to another element outside
   * or if focus lost by clicking outside. Bound to focusOut.
   *
   * @method lostFocus
   */
  lostFocus: function() {
    if(this.get('isOpen')) {
      Em.run.later(this, function() {
        var focussedElement = document.activeElement;
        var isFocussedOut = this.$().has(focussedElement).length === 0 && !this.$().is(focussedElement);

        if(isFocussedOut) {
          this.closeOptions({focus:false});
        }
      }, 0);
    }
  }.on('focusOut'),

  /**
   * Handles the keydown event once the component gains focus.
   * Bound to keydown.
   *
   * @method navigateOnKeyDown
   */
  navigateOnKeyDown: function(event) {
    var handled = false;

    switch(event.keyCode) {
      //esc
      case 27:
        this.closeOptions();
        handled = true;
        break;

      //up-arrow
      case 38:
        if(this.get('isOpen')) {
          this.get('options').setPreviousItemFocus(this.get('selectedOption'));
        }
        handled = true;
        break;

      //down-arrow
      case 40:
        if(this.get('isOpen')) {
          this.get('options').setNextItemFocus(this.get('selectedOption'));
        } else {
          this.openOptions();
        }
        handled = true;
        break;

      //enter
      case 13:
        if(this.get('isOpen')) {
          var index = this.get('options.focusIndex');
          this.get('options.options').objectAt(index).selectOption();
          this.closeOptions();
        } else {
          this.openOptions();
        }
        handled = true;
        break;
    }

    if(handled) {
      event.preventDefault();
    }
  }.on('keyDown')

});<commit_msg>Revert "Revert "Update bm-select to have an open on focus option""<commit_after>import Em from 'ember';

export default Em.Component.extend({

  tagName: 'bm-select',

  classNameBindings: ['isOpen'],

  attributeBindings: [
    'role',
    'ariaMultiselectable:aria-multiselectable',
    'ariaExpanded:aria-expanded',
    'tabindex',
    'value'
  ],

  /**
   * This flag is set to true/false based on whether the dropdown is
   * opened/closed.
   *
   * @property isOpen
   * @type Boolean
   */
  isOpen: false,

  /**
   * Open when focused. Useful to indicate focus when not using styling
   *
   * @property openOnFocus
   * @type Boolean
   */
  openOnFocus: false,

  /**
   * See http://www.w3.org/TR/wai-aria/roles#listbox
   *
   * @property role
   * @type String
   * @private
   */
  role: 'listbox',

  /**
   * Tells screenreaders whether this widget allows multiple selection.
   *
   * @property 'ariaMultiselectable'
   * @type String
   * @private
   */
  ariaMultiselectable: 'false',

  /**
   * Tells screenreaders whether this widget can be expanded.
   *
   * @property 'ariaExpanded'
   * @type String
   * @private
   */
  ariaExpanded: function() {
    return this.get('isOpen')+'';
  }.property('isOpen'),

  /**
   * Allows the component to get focus on tab press.
   *
   * @property tabindex
   * @type Number
   */
  tabindex: 0,

  /**
   * The value of the selected option. Must be a primitive value.
   *
   * @property value
   */
  value: null,

  /**
   * Reference to the BmOptionsComponent instance.
   *
   * @property options
   * @type BmOptions
   */
  options: null,

  /**
   * Reference to the selected BmOptionComponent instance.
   *
   * @property selectedOption
   * @type BmOption
   */
  selectedOption: null,

  /**
   * Register the BmOptionsComponent instance.
   *
   * @method registerOptions
   */
  registerOptions: function(options) {
    this.set('options', options);
  },

  /**
   * Select an option.
   *
   * @method selectOption
   */
  selectOption: function(item) {
    this.set('selectedOption', item);
    this.set('value', item.get('value'));
    this.sendAction('action', item.get('data'), item.get('value'));
  },

  /**
   * Toggles the dropdown, bound to click.
   *
   * @method toggleOptions
   */
  toggleOptions: function() {
    if(this.get('isOpen')) {
      this.closeOptions();
    } else {
      this.openOptions();
    }
  }.on('click'),

  /**
   * Observes focusIn so select can be opened if option set
   *
   * @method focusInObserver
   */
  focusObserver: function() {
    if(!this.get('isOpen') && this.get('openOnFocus')) {
      this.openOptions();
    }
  }.on('focusIn')

  /**
   * Opens the dropdown.
   *
   * @method openOptions
   */
  openOptions: function() {
    this.set('isOpen', true);
    this.get('options').focusItemAt(this.get('options.selectedIndex'));
  },

  /**
   * Closes the dropdown.
   *
   * @method closeOptions
   */
  closeOptions: function(options) {
    options = options || { focus: true};

    this.set('isOpen', false);
    this.set('options.focusIndex', -1);

    //set focus back to bm-select on dropdown close.
    //dont do this if focussing out or click outside.
    if(options.focus) {
      Em.run.schedule('afterRender', this, function() {
        this.$().focus();
      });
    }
  },

  /**
   * Closes the dropdown when focus changes to another element outside
   * or if focus lost by clicking outside. Bound to focusOut.
   *
   * @method lostFocus
   */
  lostFocus: function() {
    if(this.get('isOpen')) {
      Em.run.later(this, function() {
        var focussedElement = document.activeElement;
        var isFocussedOut = this.$().has(focussedElement).length === 0 && !this.$().is(focussedElement);

        if(isFocussedOut) {
          this.closeOptions({focus:false});
        }
      }, 0);
    }
  }.on('focusOut'),

  /**
   * Handles the keydown event once the component gains focus.
   * Bound to keydown.
   *
   * @method navigateOnKeyDown
   */
  navigateOnKeyDown: function(event) {
    var handled = false;

    switch(event.keyCode) {
      //esc
      case 27:
        this.closeOptions();
        handled = true;
        break;

      //up-arrow
      case 38:
        if(this.get('isOpen')) {
          this.get('options').setPreviousItemFocus(this.get('selectedOption'));
        }
        handled = true;
        break;

      //down-arrow
      case 40:
        if(this.get('isOpen')) {
          this.get('options').setNextItemFocus(this.get('selectedOption'));
        } else {
          this.openOptions();
        }
        handled = true;
        break;

      //enter
      case 13:
        if(this.get('isOpen')) {
          var index = this.get('options.focusIndex');
          this.get('options.options').objectAt(index).selectOption();
          this.closeOptions();
        } else {
          this.openOptions();
        }
        handled = true;
        break;
    }

    if(handled) {
      event.preventDefault();
    }
  }.on('keyDown')

});
<|endoftext|>


<commit_before>(function() {

//  ---------------------------------------------------------------------------------------------------------------  //
//  no.Model
//  ---------------------------------------------------------------------------------------------------------------  //

//  Базовый класс для моделей. Конструктор пустой, чтобы легче было наследоваться.
//  Вся инициализация делается в _init(), который вызывает фабрикой no.Model.create().
//
no.Model = function() {};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Добавляем методы из no.Events: on(), off(), trigger().
no.extend(no.Model.prototype, no.Events);

//  ---------------------------------------------------------------------------------------------------------------  //

var _ctors = {};
var _infos = {};

var _cache = {};

var _keySuffix = 0;

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype._init = function(id, params, data) {
    this.id = id;
    this.params = params;

    this._reset();
    this.setData(data);

    var info = this.info = _infos[id];

    this.key = no.Model.key(id, params, info);

    this.timstamp = 0;
};

no.Model.prototype._reset = function(status) {
    this.data = null;
    this.error = null;
    /*
        Возможные варианты status:

            'none'          данные еще не загружались
            'loading'       данные загружаются в данный момент
            'failed'        данные загрузились с ошибкой, нужен retry
            'error'         данные загрузились с ошибкой, retry невозможен
            'ok'            данные загрузились успешно
            'invalid'       модель вручную инвалидирована.
    */
    this.status = status || 'none';
    this.retries = 0;
    this.requests = 0;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Определяем новую модель.
//
//      //  Простая модель, без параметров.
//      no.Model.define('profile');
//
//      no.Model.define('album', {
//          params: {
//              //  Любое значение, кроме null расценивается как дефолтное значение этого параметра.
//              //  На самом деле, конечно, не любое -- смысл имеют только Number и String.
//              'author-login': null,
//              'album-id': null,
//
//              //  Этим двум параметрам заданы дефолтные значения.
//              'page': 0,
//              'pageSize': 20
//          }
//      });
//
no.Model.define = function(id, info, ctor) {
    info = info || {};
    if (info.methods) {
        //  Нужно унаследоваться от no.Model и добавить в прототип info.models.
        ctor = no.inherits(function() {}, no.Model, info.methods);
    } else {
        ctor = ctor || no.Model;
    }

    var params = info.params || (( info.params = {} ));

    info.pNames = no.object.keys(params);

    //  Для do-моделей отдельные правила кэширования и построения ключей.
    info.isDo = /^do-/.test(id);

    _infos[id] = info;
    _ctors[id] = ctor;

    //  Создаем пустой кэш для всех моделей с данным id.
    _cache[id] = {};
};

//  Фабрика для моделей. Создает инстанс нужного класса и инициализирует его.
no.Model.create = function(id, params, data)  {
    var model = no.Model.get(id, params);

    if (!model) {
        var ctor = _ctors[id];
        model = new ctor();
        model._init(id, params, data);

        no.Model.store(model);
    } else if (data) {
        //  Модель уже существует, а мы пытаемся создать такую же, но с непустой data.
        throw Error('Model already exists');
    }

    return model;
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.info = function(id) {
    return _infos[id];
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.key = function(id, params, info) {
    info || ( info = _infos[id] );

    //  Для do-моделей ключ строим особым образом.
    if (info.isDo) {
        return 'do-' + _keySuffix++;
    }

    var defaults = info.params;
    var pNames = info.pNames;

    var key = 'model=' + id;

    for (var i = 0, l = pNames.length; i < l; i++) {
        var pName = pNames[i];

        var pValue = params[pName];
        //  Нельзя просто написать params[pName] || defaults[pName] --
        //  т.к. params[pName] может быть 0 или ''.
        pValue = (pValue === undefined) ? defaults[pName] : pValue;

        if (pValue != null) {
            key += '&' + pName + '=' + pValue;
        }
    }

    return key;
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.invalidate = function() {
    this._reset('invalid');
};

no.Model.prototype.isValid = function() {
    return (this.status === 'ok');
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Возвращает данные, находящиеся по пути path.
//
//      var foo = model.get('foo'); // model.data.foo.
//      var bar = model.get('foo.bar'); // model.data.foo.bar (если foo существует).
//
no.Model.prototype.get = function(path) {
    var data = this.data;
    if (data) {
        return no.path(path, data);
    }
};

//  Сохраняет value по пути path.
//
//  Возможные флаги, которые могут быть в options:
//
//      {
//          silent: true        //  Не генерить событие о том, что модель изменилась,
//      }
//
no.Model.prototype.set = function(path, value, options) {
    var data = this.data;
    if (!data) { return; }

    //  Сохраняем новое значение и одновременно получаем старое значение.
    var oldValue = no.path(path, data, value);

    if ( !( (options && options.silent) || no.object.isEqual(value, oldValue) ) ) {
        //  FIXME: Может быть отправлять старое/новое значение?
        this.trigger('changed.' + path);
    }
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.getData = function() {
    return this.data;
};

//  Заменяет данные модели на data.
//
//  При этом по-дефолту на модели генерится событие 'changed'.
//  Это поведение можно поменять, передав объект options:
//
//      {
//          silent: true        //  Не генерить событие о том, что модель изменилась,
//      }
//
no.Model.prototype.setData = function(data, options) {
    if (data) {
        var old = this.data;
        this.data = this.preprocessData(data);
        this.error = null;
        this.status = 'ok';

        //  Не проверяем здесь, действительно ли data отличается от oldData --
        //  setData должен вызываться только когда обновленная модель целиком перезапрошена.
        //  Можно считать, что она в этом случае всегда меняется.
        if (old != null && !(options && options.silent) ) {
            this.trigger('changed');
        }
    }

};

no.Model.prototype.getError = function() {
    return this.error;
};

no.Model.prototype.setError = function(error) {
    this.data = null;
    this.error = error;
    this.status = 'failed';
};

no.Model.prototype.preprocessData = function(data) {
    return data;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  FIXME: Этот код сильно пересекается с вычислением ключа.
//  Нельзя ли избавиться от копипаста?
no.Model.prototype.getRequestParams = function() {
    var params = this.params;

    var defaults = this.info.params;
    var reqParams = {};

    for (var pName in defaults) {
        var pValue = params[pName];

        pValue = (pValue === undefined) ? defaults[pName] : pValue;
        if (pValue != null) {
            reqParams[pName] = pValue;
        }
    }

    return reqParams;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Работа с кэшем.

//  Достаем модель из кэша.
no.Model.get = function(id, key) {
    key = (typeof key === 'string') ? key : no.Model.key(id, key);

    var model = _cache[id][key];
    return model;
};

//  Сохраняем модель в кэше.
no.Model.store = function(model) {
    if ( model.isDo() ) {
        return;
    }

    var id = model.id;
    var key = model.key;

    var cached = _cache[id][key];
    if (!cached) {
        _cache[id][key] = model;
    } else {
        //  NOTE: Почему тут нельзя просто заменить старую модель на новую.
        //  Потому, что в этом случае все, кто был подписан на события от старой модели
        //  не смогут переподписаться на новую модель.
        cached.data = model.data;
    }
};

//  Проверяем, есть ли модель в кэше и валидна ли она.
no.Model.isValid = function(id, key) {
    var model = no.Model.get(id, key);
    if (!model) { return; } // undefined означает, что кэша нет вообще, а false -- что он инвалидный.

    return model.isValid();
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Возвращает, можно ли перезапрашивать эту модель, если предыдущий запрос не удался.
no.Model.prototype.canRetry = function(error) {
    //  do-модели нельзя перезапрашивать.
    return ( !this.isDo() && this.retries < 3 );
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.extractData = function(result) {
    if (result) {
        return result.result;
    }
};

no.Model.prototype.extractError = function(result) {
    if (result) {
        return result.error;
    }
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.isDo = function() {
    return this.info.isDo;
};

//  ---------------------------------------------------------------------------------------------------------------  //

})();

<commit_msg>no.Model.invalidate<commit_after>(function() {

//  ---------------------------------------------------------------------------------------------------------------  //
//  no.Model
//  ---------------------------------------------------------------------------------------------------------------  //

//  Базовый класс для моделей. Конструктор пустой, чтобы легче было наследоваться.
//  Вся инициализация делается в _init(), который вызывает фабрикой no.Model.create().
//
no.Model = function() {};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Добавляем методы из no.Events: on(), off(), trigger().
no.extend(no.Model.prototype, no.Events);

//  ---------------------------------------------------------------------------------------------------------------  //

var _ctors = {};
var _infos = {};

var _cache = {};

var _keySuffix = 0;

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype._init = function(id, params, data) {
    this.id = id;
    this.params = params;

    this._reset();
    this.setData(data);

    var info = this.info = _infos[id];

    this.key = no.Model.key(id, params, info);

    this.timstamp = 0;
};

no.Model.prototype._reset = function(status) {
    this.data = null;
    this.error = null;
    /*
        Возможные варианты status:

            'none'          данные еще не загружались
            'loading'       данные загружаются в данный момент
            'failed'        данные загрузились с ошибкой, нужен retry
            'error'         данные загрузились с ошибкой, retry невозможен
            'ok'            данные загрузились успешно
            'invalid'       модель вручную инвалидирована.
    */
    this.status = status || 'none';
    this.retries = 0;
    this.requests = 0;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Определяем новую модель.
//
//      //  Простая модель, без параметров.
//      no.Model.define('profile');
//
//      no.Model.define('album', {
//          params: {
//              //  Любое значение, кроме null расценивается как дефолтное значение этого параметра.
//              //  На самом деле, конечно, не любое -- смысл имеют только Number и String.
//              'author-login': null,
//              'album-id': null,
//
//              //  Этим двум параметрам заданы дефолтные значения.
//              'page': 0,
//              'pageSize': 20
//          }
//      });
//
no.Model.define = function(id, info, ctor) {
    info = info || {};
    if (info.methods) {
        //  Нужно унаследоваться от no.Model и добавить в прототип info.models.
        ctor = no.inherits(function() {}, no.Model, info.methods);
    } else {
        ctor = ctor || no.Model;
    }

    var params = info.params || (( info.params = {} ));

    info.pNames = no.object.keys(params);

    //  Для do-моделей отдельные правила кэширования и построения ключей.
    info.isDo = /^do-/.test(id);

    _infos[id] = info;
    _ctors[id] = ctor;

    //  Создаем пустой кэш для всех моделей с данным id.
    _cache[id] = {};
};

//  Фабрика для моделей. Создает инстанс нужного класса и инициализирует его.
no.Model.create = function(id, params, data)  {
    var model = no.Model.get(id, params);

    if (!model) {
        var ctor = _ctors[id];
        model = new ctor();
        model._init(id, params, data);

        no.Model.store(model);
    } else if (data) {
        //  Модель уже существует, а мы пытаемся создать такую же, но с непустой data.
        throw Error('Model already exists');
    }

    return model;
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.info = function(id) {
    return _infos[id];
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.key = function(id, params, info) {
    info || ( info = _infos[id] );

    //  Для do-моделей ключ строим особым образом.
    if (info.isDo) {
        return 'do-' + _keySuffix++;
    }

    var defaults = info.params;
    var pNames = info.pNames;

    var key = 'model=' + id;

    for (var i = 0, l = pNames.length; i < l; i++) {
        var pName = pNames[i];

        var pValue = params[pName];
        //  Нельзя просто написать params[pName] || defaults[pName] --
        //  т.к. params[pName] может быть 0 или ''.
        pValue = (pValue === undefined) ? defaults[pName] : pValue;

        if (pValue != null) {
            key += '&' + pName + '=' + pValue;
        }
    }

    return key;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Инвалидируем все модели с заданным id,
//  удовлетворяющие фильтру filter (функция, принимающая
//  параметром модель и возвращающая boolean).
no.Model.invalidate = function(id, filter) {
    var models = _cache[id];

    for (var key in models) {
        var model = models[key];
        if ( filter(model) ) {
            model.invalidate();
        }
    }
};

no.Model.prototype.invalidate = function() {
    this._reset('invalid');
};

no.Model.prototype.isValid = function() {
    return (this.status === 'ok');
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Возвращает данные, находящиеся по пути path.
//
//      var foo = model.get('foo'); // model.data.foo.
//      var bar = model.get('foo.bar'); // model.data.foo.bar (если foo существует).
//
no.Model.prototype.get = function(path) {
    var data = this.data;
    if (data) {
        return no.path(path, data);
    }
};

//  Сохраняет value по пути path.
//
//  Возможные флаги, которые могут быть в options:
//
//      {
//          silent: true        //  Не генерить событие о том, что модель изменилась,
//      }
//
no.Model.prototype.set = function(path, value, options) {
    var data = this.data;
    if (!data) { return; }

    //  Сохраняем новое значение и одновременно получаем старое значение.
    var oldValue = no.path(path, data, value);

    if ( !( (options && options.silent) || no.object.isEqual(value, oldValue) ) ) {
        //  FIXME: Может быть отправлять старое/новое значение?
        this.trigger('changed.' + path);
    }
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.getData = function() {
    return this.data;
};

//  Заменяет данные модели на data.
//
//  При этом по-дефолту на модели генерится событие 'changed'.
//  Это поведение можно поменять, передав объект options:
//
//      {
//          silent: true        //  Не генерить событие о том, что модель изменилась,
//      }
//
no.Model.prototype.setData = function(data, options) {
    if (data) {
        var old = this.data;
        this.data = this.preprocessData(data);
        this.error = null;
        this.status = 'ok';

        //  Не проверяем здесь, действительно ли data отличается от oldData --
        //  setData должен вызываться только когда обновленная модель целиком перезапрошена.
        //  Можно считать, что она в этом случае всегда меняется.
        if (old != null && !(options && options.silent) ) {
            this.trigger('changed');
        }
    }

};

no.Model.prototype.getError = function() {
    return this.error;
};

no.Model.prototype.setError = function(error) {
    this.data = null;
    this.error = error;
    this.status = 'failed';
};

no.Model.prototype.preprocessData = function(data) {
    return data;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  FIXME: Этот код сильно пересекается с вычислением ключа.
//  Нельзя ли избавиться от копипаста?
no.Model.prototype.getRequestParams = function() {
    var params = this.params;

    var defaults = this.info.params;
    var reqParams = {};

    for (var pName in defaults) {
        var pValue = params[pName];

        pValue = (pValue === undefined) ? defaults[pName] : pValue;
        if (pValue != null) {
            reqParams[pName] = pValue;
        }
    }

    return reqParams;
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Работа с кэшем.

//  Достаем модель из кэша.
no.Model.get = function(id, key) {
    key = (typeof key === 'string') ? key : no.Model.key(id, key);

    var model = _cache[id][key];
    return model;
};

//  Сохраняем модель в кэше.
no.Model.store = function(model) {
    if ( model.isDo() ) {
        return;
    }

    var id = model.id;
    var key = model.key;

    var cached = _cache[id][key];
    if (!cached) {
        _cache[id][key] = model;
    } else {
        //  NOTE: Почему тут нельзя просто заменить старую модель на новую.
        //  Потому, что в этом случае все, кто был подписан на события от старой модели
        //  не смогут переподписаться на новую модель.
        cached.data = model.data;
    }
};

//  Проверяем, есть ли модель в кэше и валидна ли она.
no.Model.isValid = function(id, key) {
    var model = no.Model.get(id, key);
    if (!model) { return; } // undefined означает, что кэша нет вообще, а false -- что он инвалидный.

    return model.isValid();
};

//  ---------------------------------------------------------------------------------------------------------------  //

//  Возвращает, можно ли перезапрашивать эту модель, если предыдущий запрос не удался.
no.Model.prototype.canRetry = function(error) {
    //  do-модели нельзя перезапрашивать.
    return ( !this.isDo() && this.retries < 3 );
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.extractData = function(result) {
    if (result) {
        return result.result;
    }
};

no.Model.prototype.extractError = function(result) {
    if (result) {
        return result.error;
    }
};

//  ---------------------------------------------------------------------------------------------------------------  //

no.Model.prototype.isDo = function() {
    return this.info.isDo;
};

//  ---------------------------------------------------------------------------------------------------------------  //

})();

<|endoftext|>


<commit_before>/**
 * Mountains
 */
var colorPalate = 'mountains';

Components.remove({ 'colorPalate': colorPalate });
Pages.remove({ 'colorPalate': colorPalate });

if (Components.find( { 'colorPalate': colorPalate } ).count() === 0) {

    /**
     **************************************************************************
     * Mountains
     **************************************************************************
     */
    var mountains = Components.insert({
        colorPalate: colorPalate,
        link: 'mountains',
        title: 'The Cuyamaca Mountains',
        titleEsp: 'Las montañas Cuyamaca',
        componentNum: '0816',
        order: 7,
        mural: 'mtn-mural.jpg'
    });

    Pages.insert({
        colorPalate: colorPalate,
        componentId: mountains,
        link: 'where-is-it',
        buttonTitle: 'Where is it?',
        espButtonTitle: '¿Dónde está?',
        order: 1,
        bodyCopy: 'The Cuyamaca Mountains, the setting for the scene in front of you, are just 50 miles from the Museum',
        espBodyCopy: 'Las montañas Cuyamaca, el marco de la escena delante de ti, están apenas a 50 millas del  Museo. ',
        backgroundMapClass: 'map-mtn-where',
        bodyImages: [
            {fileName: 'mtn-where.jpg'},
        ],
    });

    Pages.insert({
        colorPalate: colorPalate,
        componentId: mountains,
        link: 'our-mountains',
        buttonTitle: 'Our mountains',
        espButtonTitle: 'Nuestras montañas',
        order: 2,
        bodyCopyLayout: 'horizontal',
        bodyCopy: 'Southern California has several hundred mountain peaks, including San Gorgonio Mountain, which rises to 11,503 feet.',
        espBodyCopy: 'El sur de California tiene varios centenares de cumbres, incluyendo la montaña San Gorgonio, que tiene una elevación de 11.503 pies.',
        backgroundMapClass: 'map-mtn-our',
    });

    Pages.insert({
        colorPalate: colorPalate,
        componentId: mountains,
        link: 'rare-habitat',
        buttonTitle: 'Rare habitat',
        espButtonTitle: 'Hábitat inusual',
        order: 3,
        bodyCopyLayout: 'horizontal',
        bodyCopy: '
Our highest mountain peaks—the coolest and wettest areas of southern California—are home to some of the last conifer forests in the region.

Move the slider to compare southern California elevations to conifer forest locations.
',
        espBodyCopy: '
Nuestras cumbres más elevadas—las zonas más frescas y húmedas del sur de California—albergan algunos de los últimos bosques de coníferas en la región.

Mueve el control deslizable para comparar las elevaciones del sur de California con los sitios de bosques coníferas.
',
        backgroundMapClass: 'map-mount-rare',
        bottomImage: 'mtn-compare-elevation-01-v08.png',
        bottomImageLabel: '',
        topImage: 'mtn-compare-elevation-02-v04.png',
        topImageLabel: '',
    });

    var mtnExplore = Pages.insert({
        colorPalate: colorPalate,
        componentId: mountains,
        link: 'explore',
        buttonTitle: 'Explore',
        espButtonTitle: 'Explora',
        order: 4,
        bodyCopy: '',
        espBodyCopy: '',
        backgroundMapClass: 'map-explore',
    });

    Pages.insert({
        colorPalate: colorPalate,
        subPageType: 'explore',
        componentId: mountains,
        parentId: mtnExplore,
        link: 'palomar',
        buttonTitle: 'Palomar Mountain State Park',
        espButtonTitle: 'Parque Estatal Palomar Mountain',
        buttonImage: 'mtn-explore-palomar.jpg',
        bodyImages: [
            {
              fileName: 'mtn-explore-palomar-full.jpg',
              zoom: 'zoom-true',
            },
        ],
        order: 1,
        bodyCopy: 'Visit Palomar Mountain State Park and enjoy a panoramic view of southern California that stretches from the ocean to the desert.',
        espBodyCopy: 'Visita al Parque Estatal Palomar Mountain y disfruta de una vista panorámica del sur de California que abarca desde el océano hasta el desierto.',
        websiteCall: 'For more information, visit:',
        websiteCallEsp: 'Para más información, visita:',
        websiteURL: 'parks.ca.gov',
        backgroundMapClass: 'map-mtn-explore-palomar',
    });

    Pages.insert({
        colorPalate: colorPalate,
        subPageType: 'explore',
        componentId: mountains,
        parentId: mtnExplore,
        link: 'stonewall',
        buttonTitle: 'Cuyamaca Rancho State Park (Stonewall Peak)',
        espButtonTitle: 'Parque Estatal Cuyamaca Rancho (Pico Stonewall)',
        buttonImage: 'mtn-explore-stonewall.jpg',
        bodyImages: [
            {
              fileName: 'mtn-explore-stonewall-full.jpg',
              zoom: 'zoom-true',
            },
        ],
        order: 2,
        bodyCopy: '
Climb a switchback trail to Stonewall Peak at 5,730 feet and enjoy views of Lake Cuyamaca and nearby peaks.

Join the canyoneers for guided hikes in southern California.
',
        espBodyCopy: '
Sube por un sendero zigzagueante hasta la Cima de Stonewall a 5.730 pies y disfruta de las vistas del lago Cuyamaca y las cumbres cercanas.

Únete a los canyoneers para caminatas guiadas en el sur de California.
',
        websiteCall: 'For more information, visit:',
        websiteCallEsp: 'Para más información, visita:',
        websiteURL: 'sdnat.org/canyoneers',
        backgroundMapClass: 'map-mtn-explore-stonewall',
    });

    Pages.insert({
        colorPalate: colorPalate,
        subPageType: 'explore',
        componentId: mountains,
        parentId: mtnExplore,
        link: 'volcan',
        buttonTitle: 'Volcan Mountain Wilderness Preserve',
        espButtonTitle: 'Reserva Silvestre Volcan Mountain',
        buttonImage: 'mtn-explore-volcan.jpg',
        bodyImages: [
            {
              fileName: 'mtn-explore-volcan-full.jpg',
              zoom: 'zoom-true',
            },
        ],
        order: 3,
        bodyCopy: '
Hike alongside beds of wildflowers and under the shade of giant oak trees straight to Volcan Peak for a view of the entire preserve.

Join the canyoneers for guided hikes in southern California.
',
        espBodyCopy: '
Camina a lo largo de campos de flores silvestres y bajo la sombra de robles gigantes hasta llegar a Volcan Peak para disfrutar de una vista que abarca toda la reserva.

Únete a los canyoneers para caminatas guiadas en el sur de California.
',
        websiteCall: 'For more information, visit:',
        websiteCallEsp: 'Para más información, visita:',
        websiteURL: 'sdnat.org/canyoneers',
        backgroundMapClass: 'map-mtn-explore-volcan',
    });

    Pages.insert({
        colorPalate: colorPalate,
        subPageType: 'explore',
        componentId: mountains,
        parentId: mtnExplore,
        link: 'thunder',
        buttonTitle: 'Palomar Mountain State Park (Thunder Spring/Chimney Flats)',
        espButtonTitle: 'Parque Estatal Palomar Mountain (Thunder Spring/Chimney Flats)',
        buttonImage: 'mtn-explore-thunder.jpg',
        bodyImages: [
            {
              fileName: 'mtn-explore-thunder-full.jpg',
              zoom: 'zoom-true',
            },
        ],
        order: 4,
        bodyCopy: '
Enjoy a picturesque trail up to Thunder Spring while hiking through oak and conifer forests. It gets a little steep, but the view is worth the climb!

Join the canyoneers for guided hikes in southern California.
',
        espBodyCopy: '
Disfruta de un sendero pintoresco hasta alcanzar Thunder Spring mientras caminas por los bosques de robles y coníferas. Se torna un poco empinado, ¡pero la vista hace que la subida valga la pena!

Únete a los canyoneers para caminatas guiadas en el sur de California.
',
        websiteCall: 'For more information, visit:',
        websiteCallEsp: 'Para más información, visita:',
        websiteURL: 'sdnat.org/canyoneers',
        backgroundMapClass: 'map-mtn-explore-thunder',
    });

    var mtnScienceStories = Pages.insert({
        colorPalate: colorPalate,
        link: 'science',
        componentId: mountains,
        buttonTitle: 'Science spotlight',
        espButtonTitle: 'Enfoque científico',
        order: 5,
        bodyCopy: '',
        espBodyCopy: '',
        backgroundMapClass: 'map-stories',
    });

    Pages.insert({
        colorPalate: colorPalate,
        subPageType: 'science',
        link: 'lori-hargrove',
        componentId: mountains,
        parentId: mtnScienceStories,
        buttonTitle: 'Lori Hargrove
Ecologist',
        espButtonTitle: 'Ecologista',
        buttonImage: 'sci-hargrove-lori-button.jpg',
        bodyImages: [
            { fileName: 'sci-hargrove-lori-full.jpg' },
        ],
        order: 1,
        bodyCopy: '"The White-headed Woodpecker is found only in the pine forests of western North America, from Canada south to Cuyamaca Rancho State Park in San Diego County. The Cedar Fire in 2003, which burned 95 percent of the park, destroyed this species\' southernmost habitat."',
        espBodyCopy: '"El carpintero cabeza blanca se encuentra únicamente en los bosques de pinos en el oeste de Norteamérica, desde Canadá hasta el sur en el Parque Estatal Cuyamaca Rancho en el condado de San Diego. El incendio Cedar de 2003, que quemó el noventa y cinco por ciento del parque, destruyó el hábitat más sureño de esta especie".',
        backgroundMapClass: 'sci-mtn-woodpecker',
        backgroundCaption: 'White-headed Woodpecker',
        backgroundCaptionEsp: 'Carpintero cabeza blanca',
        backgroundCaptionLatin: 'Picoides albolarvatus'
    });

    Pages.insert({
        colorPalate: colorPalate,
        subPageType: 'science',
        link: 'jim-berrian',
        componentId: mountains,
        parentId: mtnScienceStories,
        buttonTitle: 'Jim Berrian
Entomologist',
        espButtonTitle: 'Entomólogo',
        buttonImage: 'sci-berrian-jim-button.jpg',
        bodyImages: [
            {fileName: 'sci-berrian-jim-full.jpg'},
        ],
        order: 2,
        bodyCopy: '"This native Californian spider was discovered only recently, in 2005. It’s smaller than a grain of rice. This discovery is exciting because it shows that we still have new finds to make right here in our local mountains."',
        espBodyCopy: '"Esta araña nativa de California fue recién descubierta en 2005.  Es más pequeña que un grano de arroz.  Este descubrimiento es fascinante porque demuestra que tenemos nuevas cosas para descubrir aquí mismo en nuestras montañas locales".',
        backgroundMapClass: 'sci-can-spider',
        backgroundCaption: 'Spider',
        backgroundCaptionEsp: 'Araña',
        backgroundCaptionLatin: 'Apostenus californicus'
    });

}
<commit_msg>Indentation cleanup<commit_after>/**
* Mountains
*/
var colorPalate = 'mountains';

Components.remove({ 'colorPalate': colorPalate });
Pages.remove({ 'colorPalate': colorPalate });

if (Components.find( { 'colorPalate': colorPalate } ).count() === 0) {

  /**
  **************************************************************************
  * Mountains
  **************************************************************************
  */
  var mountains = Components.insert({
    colorPalate: colorPalate,
    link: 'mountains',
    title: 'The Cuyamaca Mountains',
    titleEsp: 'Las montañas Cuyamaca',
    componentNum: '0816',
    order: 7,
    mural: 'mtn-mural.jpg'
  });

  Pages.insert({
    colorPalate: colorPalate,
    componentId: mountains,
    link: 'where-is-it',
    buttonTitle: 'Where is it?',
    espButtonTitle: '¿Dónde está?',
    order: 1,
    bodyCopy: 'The Cuyamaca Mountains, the setting for the scene in front of you, are just 50 miles from the Museum',
    espBodyCopy: 'Las montañas Cuyamaca, el marco de la escena delante de ti, están apenas a 50 millas del  Museo. ',
    backgroundMapClass: 'map-mtn-where',
    bodyImages: [
      {fileName: 'mtn-where.jpg'},
    ],
  });

  Pages.insert({
    colorPalate: colorPalate,
    componentId: mountains,
    link: 'our-mountains',
    buttonTitle: 'Our mountains',
    espButtonTitle: 'Nuestras montañas',
    order: 2,
    bodyCopyLayout: 'horizontal',
    bodyCopy: 'Southern California has several hundred mountain peaks, including San Gorgonio Mountain, which rises to 11,503 feet.',
    espBodyCopy: 'El sur de California tiene varios centenares de cumbres, incluyendo la montaña San Gorgonio, que tiene una elevación de 11.503 pies.',
    backgroundMapClass: 'map-mtn-our',
  });

  Pages.insert({
    colorPalate: colorPalate,
    componentId: mountains,
    link: 'rare-habitat',
    buttonTitle: 'Rare habitat',
    espButtonTitle: 'Hábitat inusual',
    order: 3,
    bodyCopyLayout: 'horizontal',
    bodyCopy: '
Our highest mountain peaks—the coolest and wettest areas of southern California—are home to some of the last conifer forests in the region.

Move the slider to compare southern California elevations to conifer forest locations.
',
    espBodyCopy: '
Nuestras cumbres más elevadas—las zonas más frescas y húmedas del sur de California—albergan algunos de los últimos bosques de coníferas en la región.

Mueve el control deslizable para comparar las elevaciones del sur de California con los sitios de bosques coníferas.
',
    backgroundMapClass: 'map-mount-rare',
    bottomImage: 'mtn-compare-elevation-01-v08.png',
    bottomImageLabel: '',
    topImage: 'mtn-compare-elevation-02-v04.png',
    topImageLabel: '',
  });

  var mtnExplore = Pages.insert({
    colorPalate: colorPalate,
    componentId: mountains,
    link: 'explore',
    buttonTitle: 'Explore',
    espButtonTitle: 'Explora',
    order: 4,
    bodyCopy: '',
    espBodyCopy: '',
    backgroundMapClass: 'map-explore',
  });

  Pages.insert({
    colorPalate: colorPalate,
    subPageType: 'explore',
    componentId: mountains,
    parentId: mtnExplore,
    link: 'palomar',
    buttonTitle: 'Palomar Mountain State Park',
    espButtonTitle: 'Parque Estatal Palomar Mountain',
    buttonImage: 'mtn-explore-palomar.jpg',
    bodyImages: [
      {
        fileName: 'mtn-explore-palomar-full.jpg',
        zoom: 'zoom-true',
      },
    ],
    order: 1,
    bodyCopy: 'Visit Palomar Mountain State Park and enjoy a panoramic view of southern California that stretches from the ocean to the desert.',
    espBodyCopy: 'Visita al Parque Estatal Palomar Mountain y disfruta de una vista panorámica del sur de California que abarca desde el océano hasta el desierto.',
    websiteCall: 'For more information, visit:',
    websiteCallEsp: 'Para más información, visita:',
    websiteURL: 'parks.ca.gov',
    backgroundMapClass: 'map-mtn-explore-palomar',
  });

  Pages.insert({
    colorPalate: colorPalate,
    subPageType: 'explore',
    componentId: mountains,
    parentId: mtnExplore,
    link: 'stonewall',
    buttonTitle: 'Cuyamaca Rancho State Park (Stonewall Peak)',
    espButtonTitle: 'Parque Estatal Cuyamaca Rancho (Pico Stonewall)',
    buttonImage: 'mtn-explore-stonewall.jpg',
    bodyImages: [
      {
        fileName: 'mtn-explore-stonewall-full.jpg',
        zoom: 'zoom-true',
      },
    ],
    order: 2,
    bodyCopy: '
Climb a switchback trail to Stonewall Peak at 5,730 feet and enjoy views of Lake Cuyamaca and nearby peaks.

Join the canyoneers for guided hikes in southern California.
',
    espBodyCopy: '
Sube por un sendero zigzagueante hasta la Cima de Stonewall a 5.730 pies y disfruta de las vistas del lago Cuyamaca y las cumbres cercanas.

Únete a los canyoneers para caminatas guiadas en el sur de California.
',
    websiteCall: 'For more information, visit:',
    websiteCallEsp: 'Para más información, visita:',
    websiteURL: 'sdnat.org/canyoneers',
    backgroundMapClass: 'map-mtn-explore-stonewall',
  });

  Pages.insert({
    colorPalate: colorPalate,
    subPageType: 'explore',
    componentId: mountains,
    parentId: mtnExplore,
    link: 'volcan',
    buttonTitle: 'Volcan Mountain Wilderness Preserve',
    espButtonTitle: 'Reserva Silvestre Volcan Mountain',
    buttonImage: 'mtn-explore-volcan.jpg',
    bodyImages: [
      {
        fileName: 'mtn-explore-volcan-full.jpg',
        zoom: 'zoom-true',
      },
    ],
    order: 3,
    bodyCopy: '
Hike alongside beds of wildflowers and under the shade of giant oak trees straight to Volcan Peak for a view of the entire preserve.

Join the canyoneers for guided hikes in southern California.
',
    espBodyCopy: '
Camina a lo largo de campos de flores silvestres y bajo la sombra de robles gigantes hasta llegar a Volcan Peak para disfrutar de una vista que abarca toda la reserva.

Únete a los canyoneers para caminatas guiadas en el sur de California.
',
    websiteCall: 'For more information, visit:',
    websiteCallEsp: 'Para más información, visita:',
    websiteURL: 'sdnat.org/canyoneers',
    backgroundMapClass: 'map-mtn-explore-volcan',
  });

  Pages.insert({
    colorPalate: colorPalate,
    subPageType: 'explore',
    componentId: mountains,
    parentId: mtnExplore,
    link: 'thunder',
    buttonTitle: 'Palomar Mountain State Park (Thunder Spring/Chimney Flats)',
    espButtonTitle: 'Parque Estatal Palomar Mountain (Thunder Spring/Chimney Flats)',
    buttonImage: 'mtn-explore-thunder.jpg',
    bodyImages: [
      {
        fileName: 'mtn-explore-thunder-full.jpg',
        zoom: 'zoom-true',
      },
    ],
    order: 4,
    bodyCopy: '
Enjoy a picturesque trail up to Thunder Spring while hiking through oak and conifer forests. It gets a little steep, but the view is worth the climb!

Join the canyoneers for guided hikes in southern California.
',
    espBodyCopy: '
Disfruta de un sendero pintoresco hasta alcanzar Thunder Spring mientras caminas por los bosques de robles y coníferas. Se torna un poco empinado, ¡pero la vista hace que la subida valga la pena!

Únete a los canyoneers para caminatas guiadas en el sur de California.
',
    websiteCall: 'For more information, visit:',
    websiteCallEsp: 'Para más información, visita:',
    websiteURL: 'sdnat.org/canyoneers',
    backgroundMapClass: 'map-mtn-explore-thunder',
  });

  var mtnScienceStories = Pages.insert({
    colorPalate: colorPalate,
    link: 'science',
    componentId: mountains,
    buttonTitle: 'Science spotlight',
    espButtonTitle: 'Enfoque científico',
    order: 5,
    bodyCopy: '',
    espBodyCopy: '',
    backgroundMapClass: 'map-stories',
  });

  Pages.insert({
    colorPalate: colorPalate,
    subPageType: 'science',
    link: 'lori-hargrove',
    componentId: mountains,
    parentId: mtnScienceStories,
    buttonTitle: 'Lori Hargrove
Ecologist',
    espButtonTitle: 'Ecologista',
    buttonImage: 'sci-hargrove-lori-button.jpg',
    bodyImages: [
      { fileName: 'sci-hargrove-lori-full.jpg' },
    ],
    order: 1,
    bodyCopy: '"The White-headed Woodpecker is found only in the pine forests of western North America, from Canada south to Cuyamaca Rancho State Park in San Diego County. The Cedar Fire in 2003, which burned 95 percent of the park, destroyed this species\' southernmost habitat."',
    espBodyCopy: '"El carpintero cabeza blanca se encuentra únicamente en los bosques de pinos en el oeste de Norteamérica, desde Canadá hasta el sur en el Parque Estatal Cuyamaca Rancho en el condado de San Diego. El incendio Cedar de 2003, que quemó el noventa y cinco por ciento del parque, destruyó el hábitat más sureño de esta especie".',
    backgroundMapClass: 'sci-mtn-woodpecker',
    backgroundCaption: 'White-headed Woodpecker',
    backgroundCaptionEsp: 'Carpintero cabeza blanca',
    backgroundCaptionLatin: 'Picoides albolarvatus'
  });

  Pages.insert({
    colorPalate: colorPalate,
    subPageType: 'science',
    link: 'jim-berrian',
    componentId: mountains,
    parentId: mtnScienceStories,
    buttonTitle: 'Jim Berrian
Entomologist',
    espButtonTitle: 'Entomólogo',
    buttonImage: 'sci-berrian-jim-button.jpg',
    bodyImages: [
      {fileName: 'sci-berrian-jim-full.jpg'},
    ],
    order: 2,
    bodyCopy: '"This native Californian spider was discovered only recently, in 2005. It’s smaller than a grain of rice. This discovery is exciting because it shows that we still have new finds to make right here in our local mountains."',
    espBodyCopy: '"Esta araña nativa de California fue recién descubierta en 2005.  Es más pequeña que un grano de arroz.  Este descubrimiento es fascinante porque demuestra que tenemos nuevas cosas para descubrir aquí mismo en nuestras montañas locales".',
    backgroundMapClass: 'sci-can-spider',
    backgroundCaption: 'Spider',
    backgroundCaptionEsp: 'Araña',
    backgroundCaptionLatin: 'Apostenus californicus'
  });

}
<|endoftext|>


<commit_before>/**
 * Superstore synchronous library
 *
 * @author Matt Andrews <matthew.andrews@ft.com>
 * @copyright The Financial Times [All Rights Reserved]
 */

var escapeRegex = require('escape-regexp-component');

var keys = {};
var store = {};
var persist = true;

// Watch for changes from other tabs
window.addEventListener("storage", function(e) {
  if(keys[e.key]) {
    keys[e.key] = true;
    store[e.key] = JSON.parse(e.newValue);
  }
});

/**
 * get localstorage value for key falling back to in memory for iOS private browsing bug
 * <http://stackoverflow.com/questions/9077101/iphone-localstorage-quota-exceeded-err-issue>
 * @param {String} key
 * @return {*} data for supplied key
 *
 */
exports.get = function(key) {
  if (!keys[key] && persist) {
    var data;
    try {
      data = localStorage[key];
    } catch(e) {
      persist = false; // Safari 8 with Cookies set to 'Never' throws on every read
    }

    // Slightly weird hack because JSON.parse of an undefined value throws
    // a weird exception "SyntaxError: Unexpected token u"
    if (data) data = JSON.parse(data);
    store[key] = data;
    keys[key] = true;
  }
  return store[key];
};

/**
 * set localstorage key,value falling back to in memory for iOS private browsing bug
 * <http://stackoverflow.com/questions/9077101/iphone-localstorage-quota-exceeded-err-issue>
 * @param {String} key
 * @param {*} value which will be passed via JSON.stringify
 * @return {*} value
 *
 */
exports.set = function(key, value) {
  if (persist) {
    try {
      localStorage[key] = JSON.stringify(value);
    } catch(err) {

      // Known iOS Private Browsing Bug - fall back to non-persistent storage
      if (err.code === 22) {
        persist = false;
      } else {
        throw err;
      }
    }
  }

  store[key] = value;
  keys[key] = true;
  return value;
};

/**
 * unset value in store for key
 * @param {String} key
 */
exports.unset = function(key) {
  delete store[key];
  delete keys[key];
  localStorage.removeItem(key);
};

/**
 * clear localstorage
 * @param clearPrefix will clear keys starting with `clearPrefix`
 * #clear(true) and #clear() clear cache and persistent layer, #clear(false) only clears cache
 *
 */
exports.clear = function(clearPrefix) {
  var key;
  if(!clearPrefix) {
    if (persist) {
      localStorage.clear();
    }
    store = {};
    keys = {};
    return;
  }

  clearPrefix = escapeRegex(clearPrefix);
  var clearKeyRegex = new RegExp("^"+clearPrefix);
  for(key in keys) {
    if(key.match(clearKeyRegex)) {
      this.unset(key);
    }
  }
};
<commit_msg>Catch invalid uses of get/set<commit_after>/**
 * Superstore synchronous library
 *
 * @author Matt Andrews <matthew.andrews@ft.com>
 * @copyright The Financial Times [All Rights Reserved]
 */

var escapeRegex = require('escape-regexp-component');

var keys = {};
var store = {};
var persist = true;

// Watch for changes from other tabs
window.addEventListener("storage", function(e) {
  if(keys[e.key]) {
    keys[e.key] = true;
    store[e.key] = JSON.parse(e.newValue);
  }
});

/**
 * get localstorage value for key falling back to in memory for iOS private browsing bug
 * <http://stackoverflow.com/questions/9077101/iphone-localstorage-quota-exceeded-err-issue>
 * @param {String} key
 * @return {*} data for supplied key
 *
 */
exports.get = function(key) {
  if (arguments.length !== 1) {
    throw Error("get expects 1 argument, " + arguments.length + " given; " + key);
  }
  if (!keys[key] && persist) {
    var data;
    try {
      data = localStorage[key];
    } catch(e) {
      persist = false; // Safari 8 with Cookies set to 'Never' throws on every read
    }

    // Slightly weird hack because JSON.parse of an undefined value throws
    // a weird exception "SyntaxError: Unexpected token u"
    if (data) data = JSON.parse(data);
    store[key] = data;
    keys[key] = true;
  }
  return store[key];
};

/**
 * set localstorage key,value falling back to in memory for iOS private browsing bug
 * <http://stackoverflow.com/questions/9077101/iphone-localstorage-quota-exceeded-err-issue>
 * @param {String} key
 * @param {*} value which will be passed via JSON.stringify
 * @return {*} value
 *
 */
exports.set = function(key, value) {
  if (arguments.length !== 2) {
    throw Error("set expects 2 arguments, " + arguments.length + " given; " + key);
  }
  if (persist) {
    try {
      localStorage[key] = JSON.stringify(value);
    } catch(err) {

      // Known iOS Private Browsing Bug - fall back to non-persistent storage
      if (err.code === 22) {
        persist = false;
      } else {
        throw err;
      }
    }
  }

  store[key] = value;
  keys[key] = true;
  return value;
};

/**
 * unset value in store for key
 * @param {String} key
 */
exports.unset = function(key) {
  delete store[key];
  delete keys[key];
  localStorage.removeItem(key);
};

/**
 * clear localstorage
 * @param clearPrefix will clear keys starting with `clearPrefix`
 * #clear(true) and #clear() clear cache and persistent layer, #clear(false) only clears cache
 *
 */
exports.clear = function(clearPrefix) {
  var key;
  if(!clearPrefix) {
    if (persist) {
      localStorage.clear();
    }
    store = {};
    keys = {};
    return;
  }

  clearPrefix = escapeRegex(clearPrefix);
  var clearKeyRegex = new RegExp("^"+clearPrefix);
  for(key in keys) {
    if(key.match(clearKeyRegex)) {
      this.unset(key);
    }
  }
};
<|endoftext|>


<commit_before>const express = require('express');
const bodyParser = require('body-parser');
const ChannelConfig = require('./Models/ChannelConfig');
const GitlabEventHandler = require('./Gitlab/EventHandler');
const bot = require('./Discord');

const app = express();
const port = process.env.YAPPY_GITLAB_PORT || process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || 8080;
const ip = process.env.YAPPY_GITLAB_IP || process.env.OPENSHIFT_NODEJS_IP || process.env.IP || null;
const statuses = ['Online', 'Connecting', 'Reconnecting', 'Idle', 'Nearly', 'Offline'];
const statusColors = ['lightgreen', 'orange', 'orange', 'orange', 'green', 'red'];

app.set('view engine', 'hbs');

app.use(bodyParser.json({
  limit: '250kb',
}));

app.get('/', (req, res) => {
  const repos = new Set(ChannelConfig._data.reduce((a, b) => a.concat(b.repos), []));
  const status = statuses[bot.status];
  const statusColor = statusColors[bot.status];
  res.render('index', { bot, repos, status, statusColor });
});

app.post('/', (req, res) => {
  const event = req.headers['x-gitlab-event'];
  const eventName = event && event.replace(` Hook`, '').replace(/ /g, '_').toLowerCase();
  const data = req.body;

  if (!event || !data || !data.project) return res.status(403).send('Invalid data. Plz use Gitlab webhooks.');

  const repo = data.project.path_with_namespace;
  const channels = ChannelConfig.FindByRepo(repo);
  const actionText = data.object_attributes && data.object_attributes.action ? `/${data.object_attributes.action}` : '';
  Log.verbose(`GitLab | ${repo} - ${eventName}${actionText}`);
  res.send(`${repo} : Received ${eventName}${actionText}, emitting to ${channels.size} channels...`);
  const eventResponse = GitlabEventHandler.use(data, event);

  const handleError = (resp, channel) => {
    const err = resp && resp.body;
    const errors = ['Forbidden', 'Missing Access'];
    if (!res || !err) return;
    if (errors.includes(err.message) || (err.error && errors.includes(err.error.message))) {
      channel.guild.owner.sendMessage(`**ERROR: ** Yappy GitLab doesn't have permissions to read/send messages in ${channel}`);
    } else {
      Log.error(err);
    }
  };

  channels.forEach(conf => {
    let wantsEmbed = !!conf.embed;
    let { channelId, disabledEvents } = conf;
    let channel = bot.channels.get(channelId);
    if (!channel || disabledEvents.includes(event) || disabledEvents.includes(`${event}${actionText}`)) return;

    if (wantsEmbed) {
      channel.sendMessage('', { embed: eventResponse.embed }).catch(err => handleError(err, channel));
    } else {
      channel.sendMessage(`**${repo}**: ${eventResponse.text}`).catch(err => handleError(err, channel));
    }
  });
});

app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
  if (err) Log.error(err);
  res.status(500);
  res.send(err.stack);
});

app.listen(port, ip, () => {
  Log.info(`Express | Listening on ${ip || 'localhost'}:${port}`);
});

module.exports = app;
<commit_msg>fix(web): fix channels with repo handling, expected `channelId` instead of `channelID`<commit_after>const express = require('express');
const bodyParser = require('body-parser');
const ChannelConfig = require('./Models/ChannelConfig');
const GitlabEventHandler = require('./Gitlab/EventHandler');
const bot = require('./Discord');

const app = express();
const port = process.env.YAPPY_GITLAB_PORT || process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || 8080;
const ip = process.env.YAPPY_GITLAB_IP || process.env.OPENSHIFT_NODEJS_IP || process.env.IP || null;
const statuses = ['Online', 'Connecting', 'Reconnecting', 'Idle', 'Nearly', 'Offline'];
const statusColors = ['lightgreen', 'orange', 'orange', 'orange', 'green', 'red'];

app.set('view engine', 'hbs');

app.use(bodyParser.json({
  limit: '250kb',
}));

app.get('/', (req, res) => {
  const repos = new Set(ChannelConfig._data.reduce((a, b) => a.concat(b.repos), []));
  const status = statuses[bot.status];
  const statusColor = statusColors[bot.status];
  res.render('index', { bot, repos, status, statusColor });
});

app.post('/', (req, res) => {
  const event = req.headers['x-gitlab-event'];
  const eventName = event && event.replace(` Hook`, '').replace(/ /g, '_').toLowerCase();
  const data = req.body;

  if (!event || !data || !data.project) return res.status(403).send('Invalid data. Plz use Gitlab webhooks.');

  const repo = data.project.path_with_namespace;
  const channels = ChannelConfig.FindByRepo(repo);
  const actionText = data.object_attributes && data.object_attributes.action ? `/${data.object_attributes.action}` : '';
  Log.verbose(`GitLab | ${repo} - ${eventName}${actionText}`);
  res.send(`${repo} : Received ${eventName}${actionText}, emitting to ${channels.size} channels...`);
  const eventResponse = GitlabEventHandler.use(data, event);

  const handleError = (resp, channel) => {
    const err = resp && resp.body;
    const errors = ['Forbidden', 'Missing Access'];
    if (!res || !err) return;
    if (errors.includes(err.message) || (err.error && errors.includes(err.error.message))) {
      channel.guild.owner.send(`**ERROR: ** Yappy GitLab doesn't have permissions to read/send messages in ${channel}`);
    } else {
      Log.error(err);
    }
  };

  channels.forEach(conf => {
    const wantsEmbed = !!conf.embed;
    const { channelID, disabledEvents } = conf;
    const channel = bot.channels.get(channelID);
    if (!channel || disabledEvents.includes(event) || disabledEvents.includes(`${event}${actionText}`)) return;

    if (wantsEmbed) {
      channel.send({ embed: eventResponse.embed }).catch(err => handleError(err, channel));
    } else {
      channel.send(`**${repo}**: ${eventResponse.text}`).catch(err => handleError(err, channel));
    }
  });
});

app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
  if (err) Log.error(err);
  res.status(500);
  res.send(err.stack);
});

app.listen(port, ip, () => {
  Log.info(`Express | Listening on ${ip || 'localhost'}:${port}`);
});

module.exports = app;
<|endoftext|>


<commit_before>// ==UserScript==
// @name        Nodesquit Nodeshot
// @namespace   leone@inventati.org
// @include     http://map.ninux.org*
// @version     1
// @grant       none
// ==/UserScript==

if(!window.jQuery) {

  console.error('ERR: jQuery not loaded.');

} else {

  function nodeLatLon(node, json) {
    var r = []
    $.each(json, function(index, value){
      $.each(value, function(index, value){
        if(value.name == node){
          r = [value.lat, value.lng];
          return false;
        }
      });
    });
    return r;
  }

  function genUrl(n, m) {
    var url = '';

    return url;
  }

  function pulisci(text) {
    var n = text.indexOf('-');
    var m = text.indexOf(':');

    m = text.substring(n+2, m);
    n = text.substring(0, n-1);

    return [n, m];
  }

  function appendBtn() {
    $('div.distance-link').each(function(index, element){
      if(!($(element).hasClass('squit'))){

        var nodi = pulisci($(element).text());
        var n = nodi[0];
        var m = nodi[1];

        $.get('http://map.ninux.org/nodes.json', function(data){

          n = nodeLatLon(n, data);
          m = nodeLatLon(m, data);

          var hwt = 'http://www.heywhatsthat.com/profiler.html';
          url = hwt+'?na='+n[0]+'&no='+n[1]+'&ma='+m[0]+'&mo='+m[1];

          $(element).addClass('squit');

          var btn = 'profilo';
          $(element).append(btn);

        }, 'json').fail(function(){
          console.error('Failed to get nodes.json');
        });

      }
    });

  }

  console.log('NSOne');

  $(window).load(function(){
    setInterval(appendBtn, 2000);
  });

}
<commit_msg>nodesquit-nodeshot: better parsing, now supports nodes with colon(s) and hyphens in their names.<commit_after>// ==UserScript==
// @name        Nodesquit Nodeshot
// @namespace   leone@inventati.org
// @include     http://map.ninux.org*
// @version     1
// @grant       none
// ==/UserScript==

if(!window.jQuery) {

  console.error('ERR: jQuery not loaded.');

} else {

  function nodeLatLon(node, json) {
    var r = []
    $.each(json, function(index, value){
      $.each(value, function(index, value){
        if(value.name == node){
          r = [value.lat, value.lng];
          return false;
        }
      });
    });
    return r;
  }

  function genUrl(n, m) {
    var url = '';

    return url;
  }

  function parse(text) {
    var node1 = text.substring(0, text.search(' - '));
    var node2 = text.substring(text.search(' - ') + 3, text.search(': '));

    return [node1, node2];
  }

  function appendBtn() {
    $('div.distance-link').each(function(index, element){
      if(!($(element).hasClass('squit'))){

        var nodi = parse($(element).text());
        var n = nodi[0];
        var m = nodi[1];

        $.get('http://map.ninux.org/nodes.json', function(data){

          n = nodeLatLon(n, data);
          m = nodeLatLon(m, data);

          var hwt = 'http://www.heywhatsthat.com/profiler.html';
          url = hwt+'?na='+n[0]+'&no='+n[1]+'&ma='+m[0]+'&mo='+m[1];

          $(element).addClass('squit');

          var btn = 'profilo';
          $(element).append(btn);

        }, 'json').fail(function(){
          console.error('Failed to get nodes.json');
        });

      }
    });

  }

  console.log('NSOne');

  $(window).load(function(){
    setInterval(appendBtn, 2000);
  });

}
<|endoftext|>


<commit_before>
module("About Regular Expressions (topics/about_regular_expressions.js)");

test("exec", function() {
    var numberFinder = /(\d).*(\d)/;
    var results = numberFinder.exec("what if 6 turned out to be 9?");
    ok(results.equalTo([__, __, __]), 'what is the value of results?');
});

test("test", function() {
    var containsSelect = /select/.test("  select * from users ");
    equal(__, containsSelect, 'does the string provided contain "select"?');
});

test("match", function() {
    var matches = "what if 6 turned out to be 9?".match(/(\d)/g);
    ok(matches.equalTo([__, __]), 'what is the value of matches?');
});

test("replace", function() {
    var pie = "apple pie".replace("apple", "strawberry");
    equal(__, pie, 'what is the value of pie?');

    pie = "what if 6 turned out to be 9?".replace(/\d/g, function(number) { // the second parameter can be a string or a function
        var map = {'6': 'six','9': 'nine'};
        return map[number];
    });
    equal(__, pie, 'what is the value of pie?');
});

// THE END
<commit_msg>Complete regular expressions tests to pass<commit_after>
module("About Regular Expressions (topics/about_regular_expressions.js)");

test("exec", function() {
    var numberFinder = /(\d).*(\d)/;
    var results = numberFinder.exec("what if 6 turned out to be 9?");
    ok(results.equalTo(['6 turned out to be 9', '6', '9']), 'what is the value of results?');
});

test("test", function() {
    var containsSelect = /select/.test("  select * from users ");
    equal(true, containsSelect, 'does the string provided contain "select"?');
});

test("match", function() {
    var matches = "what if 6 turned out to be 9?".match(/(\d)/g);
    ok(matches.equalTo(['6', '9']), 'what is the value of matches?');
});

test("replace", function() {
    var pie = "apple pie".replace("apple", "strawberry");
    equal("strawberry pie", pie, 'what is the value of pie?');

    pie = "what if 6 turned out to be 9?".replace(/\d/g, function(number) { // the second parameter can be a string or a function
        var map = {'6': 'six','9': 'nine'};
        return map[number];
    });
    equal("what if six turned out to be nine?", pie, 'what is the value of pie?');
});

// THE END
<|endoftext|>


<commit_before>export default {
  name: 'mathlive-mathfield',
  template: '
<slot></slot>
',
  props: {
    id: {
      type: String,
      default: '',
    },
    value: {
      type: String,
      default: '',
    },
    options: {
      type: Object,
      default: () => ({}),
    },
    onKeystroke: {
      type: Function,
      default(_keystroke, _ev) {
        return true;
      },
    },
    onMoveOutOf: {
      type: Function,
      default(_direction) {
        return true;
      },
    },
    onTabOutOf: {
      type: Function,
      default(_direction) {
        return true;
      },
    },
  },
  /*
   * To register this component, call:
   * ```
   *     import * as MathLive from './mathlive.mjs';
   *     import MathfieldComponent from './vue-mathlive.mjs';
   *     Vue.use(MathfieldComponent, MathLive);
   * ```
   *
   * The HTML tag for this component is `<mathlive-mathfield>`
   *
   * @param {object} vue - This module, as returned from an import statement
   * @param {object} mathlive - The MathLive module, as returned from an import
   * statement
   */
  install(vue, mathlive) {
    // When the component is installed (with Vue.use()), the first argument
    // should be the component (as received from import) and the second
    // should be the MathLive module (also as received from import).
    // The MathLive module then gets stashed as a property of the Vue
    // object to be accessed later by the component implementation.
    // This allows the user of the component to control which version of
    // the MathLive module gets used.
    Object.defineProperty(vue.prototype, '$mathlive', { value: mathlive });
    vue.component('mathlive-mathfield', this);
  },
  watch: {
    value(newValue, oldValue) {
      // When the `value` prop (from the model) is modified
      // update the mathfield to stay in sync, but don't send back content
      // change notifications, to avoid infinite loops.
      if (newValue !== oldValue) {
        this.$el.mathfield.setValue(newValue, {
          suppressChangeNotifications: true,
        });
      }
    },
    options: {
      deep: true,
      handler(newValue, oldValue) {
        if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
          this.$el.mathfield.setOptions(newValue);
        }
      },
    },
  },
  mounted() {
    // A new instance is being created
    // Wait until the DOM has been constructed...
    this.$nextTick(() => {
      // ... then make the Mathfield
      this.$mathlive.makeMathField(this.$el, {
        ...this.options,
        // To support the 'model' directive, this handler will connect
        // the content of the mathfield to the ViewModel
        onContentDidChange: (_) => {
          // When the mathfield is updated, notify the model.
          // The initial input value is generated from the <slot>
          // content, so it may need to be updated.
          this.$emit('input', this.$el.mathfield.getValue());
        },
        // Those asynchronous notification handlers are translated to events
        onFocus: (_) => {
          this.$emit('focus');
        },
        onBlur: (_) => {
          this.$emit('blur');
        },
        onUndoStateDidChange: (_, command) => {
          this.$emit('undo-state-did-change', command);
        },
        onReadAloudStatus: (_, status) => {
          this.$emit('read-aloud-status-change', status);
        },

        // Those notification handlers expect an answer back, so translate
        // them to callbacks via props
        onKeystroke: (_, keystroke, ev) => {
          return this.onKeystroke(keystroke, ev);
        },
        onMoveOutOf: (_, direction) => {
          return this.onMoveOutOf(direction);
        },
        onTabOutOf: (_, direction) => {
          return this.onTabOutOf(direction);
        },
      });
    });
  },
  methods: {
    /*
     *
     * @param {string} selector
     */
    executeCommand(selector) {
      this.$el.mathfield.executeCommand(selector);
    },
    /*
     * @return {boolean}
     */
    hasFocus() {
      return this.$el.mathfield.hasFocus();
    },
    focus() {
      this.$el.mathfield.focus();
    },
    blur() {
      this.$el.mathfield.blur();
    },
    getValue(format) {
      return this.$el.mathfield.getValue(format);
    },
    /** @deprecated */
    selectedText(format) {
      return this.$el.mathfield.$selectedText(format);
    },
    insert(text, options) {
      this.$el.mathfield.insert(text, options);
    },
    select() {
      this.$el.mathfield.select();
    },
  },
};
<commit_msg>fix: fixed #822 do not change the value of the mathfield if it is already up to date<commit_after>export default {
  name: 'mathlive-mathfield',
  template: '
<slot></slot>
',
  props: {
    id: {
      type: String,
      default: '',
    },
    value: {
      type: String,
      default: '',
    },
    options: {
      type: Object,
      default: () => ({}),
    },
    onKeystroke: {
      type: Function,
      default(_keystroke, _ev) {
        return true;
      },
    },
    onMoveOutOf: {
      type: Function,
      default(_direction) {
        return true;
      },
    },
    onTabOutOf: {
      type: Function,
      default(_direction) {
        return true;
      },
    },
  },
  /*
   * To register this component, call:
   * ```
   *     import * as MathLive from './mathlive.mjs';
   *     import MathfieldComponent from './vue-mathlive.mjs';
   *     Vue.use(MathfieldComponent, MathLive);
   * ```
   *
   * The HTML tag for this component is `<mathlive-mathfield>`
   *
   * @param {object} vue - This module, as returned from an import statement
   * @param {object} mathlive - The MathLive module, as returned from an import
   * statement
   */
  install(vue, mathlive) {
    // When the component is installed (with Vue.use()), the first argument
    // should be the component (as received from import) and the second
    // should be the MathLive module (also as received from import).
    // The MathLive module then gets stashed as a property of the Vue
    // object to be accessed later by the component implementation.
    // This allows the user of the component to control which version of
    // the MathLive module gets used.
    Object.defineProperty(vue.prototype, '$mathlive', { value: mathlive });
    vue.component('mathlive-mathfield', this);
  },
  watch: {
    value(newValue, _oldValue) {
      // When the `value` prop (from the model) is modified
      // update the mathfield to stay in sync, but don't send back content
      // change notifications, to avoid infinite loops.
      const oldMathfieldValue = this.$el.mathfield.getValue();
      if (newValue !== oldMathfieldValue) {
        this.$el.mathfield.setValue(newValue, {
          suppressChangeNotifications: true,
        });
      }
    },
    options: {
      deep: true,
      handler(newValue, oldValue) {
        if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
          this.$el.mathfield.setOptions(newValue);
        }
      },
    },
  },
  mounted() {
    // A new instance is being created
    // Wait until the DOM has been constructed...
    this.$nextTick(() => {
      // ... then make the Mathfield
      this.$mathlive.makeMathField(this.$el, {
        ...this.options,
        // To support the 'model' directive, this handler will connect
        // the content of the mathfield to the ViewModel
        onContentDidChange: (_) => {
          // When the mathfield is updated, notify the model.
          // The initial input value is generated from the <slot>
          // content, so it may need to be updated.
          this.$emit('input', this.$el.mathfield.getValue());
        },
        // Those asynchronous notification handlers are translated to events
        onFocus: (_) => {
          this.$emit('focus');
        },
        onBlur: (_) => {
          this.$emit('blur');
        },
        onUndoStateDidChange: (_, command) => {
          this.$emit('undo-state-did-change', command);
        },
        onReadAloudStatus: (_, status) => {
          this.$emit('read-aloud-status-change', status);
        },

        // Those notification handlers expect an answer back, so translate
        // them to callbacks via props
        onKeystroke: (_, keystroke, ev) => {
          return this.onKeystroke(keystroke, ev);
        },
        onMoveOutOf: (_, direction) => {
          return this.onMoveOutOf(direction);
        },
        onTabOutOf: (_, direction) => {
          return this.onTabOutOf(direction);
        },
      });
    });
  },
  methods: {
    /*
     *
     * @param {string} selector
     */
    executeCommand(selector) {
      this.$el.mathfield.executeCommand(selector);
    },
    /*
     * @return {boolean}
     */
    hasFocus() {
      return this.$el.mathfield.hasFocus();
    },
    focus() {
      this.$el.mathfield.focus();
    },
    blur() {
      this.$el.mathfield.blur();
    },
    getValue(format) {
      return this.$el.mathfield.getValue(format);
    },
    /** @deprecated */
    selectedText(format) {
      return this.$el.mathfield.$selectedText(format);
    },
    insert(text, options) {
      this.$el.mathfield.insert(text, options);
    },
    select() {
      this.$el.mathfield.select();
    },
  },
};
<|endoftext|>


<commit_before>/*
 * about.js - A more indepth and specific information about the bot.
 *
 * Contributed by Capuccino
 */

const fs = require('fs');

exports.commands = [
    'about'
];

exports.about = {
    desc: 'All you need to know where the bot came from.',
    main: (bot, ctx) => {
        return new Promise((resolve, reject) => {
            let version = JSON.parse(fs.readFileSync(`${__baseDir}/package.json`)).version;
            ctx.msg.channel.createMessage({embed: {
                title: 'About Clara',
                description: localeManager.t('about-nya', ctx.settings.locale, {github: 'https://github.com/awau/Clara', patreon: 'https://patreon.com/capuccino'}),
                image: {url: 'https://github.com/awau/Clara/raw/master/nodebot_logo.png'},
                footer: {text: `${bot.user.username} uses Clara ${version}`}
            }}).then(resolve).catch(reject);
        });
    }
};<commit_msg>Fix about<commit_after>/*
 * about.js - A more indepth and specific information about the bot.
 *
 * Contributed by Capuccino and Ovyerus
 */

const fs = require('fs');
const path = require('path');

let version;
try {
    version = JSON.parse(fs.readFileSync(path.normalize(`${__baseDir}/../package.json`))).version;
} catch(_) {
    version = JSON.parse(fs.readFileSync(`${__baseDir}/package.json`)).version;
}

exports.commands = [
    'about'
];

exports.about = {
    desc: 'All you need to know where the bot came from.',
    main: (bot, ctx) => {
        return new Promise((resolve, reject) => {
            ctx.msg.channel.createMessage({embed: {
                title: 'About Clara',
                description: localeManager.t('about-nya', ctx.settings.locale, {github: 'https://github.com/awau/Clara', patreon: 'https://patreon.com/capuccino'}),
                image: {url: 'https://github.com/awau/Clara/raw/master/nodebot_logo.png'},
                footer: {text: `${bot.user.username} uses Clara ${version}`}
            }}).then(resolve).catch(reject);
        });
    }
};<|endoftext|>


<commit_before>'use strict';

angular.module('publisherApp')
    .controller('TargetsPublishController', function ($scope, $state, $filter, Subject, Group, User, loadedTargets) {
        $scope.$parent.targets = $scope.$parent.targets || [];

        $scope.selectedtargets = $scope.$parent.targets;

        $scope.search = {target: {}};

        if (angular.equals([],$scope.$parent.targets) && loadedTargets) {
            //console.log('loaded targets :' + JSON.stringify(loadedTargets));
            // on récupère le subject car pas utile de mettre le subscribeType car FORCED par défaut
            if (loadedTargets.length > 0) {
                for (var i = 0; i < loadedTargets.length; ++i) {
                    $scope.$parent.targets.push(angular.copy(loadedTargets[i].subject));
                }
            }
        }

        $scope.$watch('search.target', function(newData, oldData) {
            console.log("search selected :", newData, oldData);
            if (angular.isDefined(newData) && !angular.equals({},newData) && newData.hasOwnProperty('modelId') && !containsSubcriber(newData.modelId)) {
                var target = {modelId :newData.modelId, displayName: newData.displayName};
                $scope.$parent.targets.push(target);

            }
        });

        /*$scope.submitUsers = function() {
            $scope.selectedtargets.push.apply($scope.selectedtargets,$scope.checkedUsers);
            $scope.$parent.targets = [];
            for(var i = 0; i < $scope.selectedtargets.length; ++i) {
                var target = {modelId :$scope.selectedtargets[i].modelId, displayName: $scope.selectedtargets[i].displayName};
                $scope.$parent.targets.push(target);
            }
            $scope.checkedUsers = [];
            $scope.type="";
            $("#userListModal").modal("hide");
        };

        $scope.submitGroups = function() {
            $scope.selectedtargets.push.apply($scope.selectedtargets,$scope.checkedGroups);
            $scope.$parent.targets = [];
            for(var i = 0; i < $scope.selectedtargets.length; ++i) {
                var target = {modelId :$scope.selectedtargets[i].modelId, displayName: $scope.selectedtargets[i].displayName};
                $scope.$parent.targets.push(target);
            }
            $scope.checkedGroups = [];
            $scope.type="";
        };*/

        $scope.remove = function(subject) {
            /*$scope.selectedtargets = $scope.selectedtargets.filter(function(element) {
                return element.modelId.keyId != subject.modelId.keyId;
            });*/
            $scope.$parent.targets = $scope.$parent.targets.filter(function(element) {
                return element.modelId.keyId != subject.modelId.keyId;
            });
        };

        $scope.viewSubject= function(subject) {
            if (subject.modelId) {
                $state.go('publish.targets.subject', subject.modelId);
            }
            return false;
        };

        function containsSubcriber (subjectKey) {
            var list = $scope.$parent.targets;
            if (list.length > 0) {
                for (var i = 0, size = list.length; i < size; i++) {
                    //console.log("checking equals ", list[i], subjectKey);
                    if (angular.equals(list[i].modelId, subjectKey)) {
                        return true;
                    }
                }
            }
            return false;
        }
    });

<commit_msg>Bug: solve potential error on removing selected targets when publishing, a better filter is done<commit_after>'use strict';

angular.module('publisherApp')
    .controller('TargetsPublishController', function ($scope, $state, $filter, Subject, Group, User, loadedTargets) {
        $scope.$parent.targets = $scope.$parent.targets || [];

        $scope.selectedtargets = $scope.$parent.targets;

        $scope.search = {target: {}};

        if (angular.equals([],$scope.$parent.targets) && loadedTargets) {
            //console.log('loaded targets :' + JSON.stringify(loadedTargets));
            // on récupère le subject car pas utile de mettre le subscribeType car FORCED par défaut
            if (loadedTargets.length > 0) {
                for (var i = 0; i < loadedTargets.length; ++i) {
                    $scope.$parent.targets.push(angular.copy(loadedTargets[i].subject));
                }
            }
        }

        $scope.$watch('search.target', function(newData, oldData) {
            console.log("search selected :", newData, oldData);
            if (angular.isDefined(newData) && !angular.equals({},newData) && newData.hasOwnProperty('modelId') && !containsSubcriber(newData.modelId)) {
                var target = {modelId :newData.modelId, displayName: newData.displayName};
                $scope.$parent.targets.push(target);

            }
        });

        $scope.remove = function(subject) {
            $scope.$parent.targets = $scope.$parent.targets.filter(function(element) {
                return !angular.equals(element.modelId, subject.modelId);
            });
        };

        $scope.viewSubject= function(subject) {
            if (subject.modelId) {
                $state.go('publish.targets.subject', subject.modelId);
            }
            return false;
        };

        function containsSubcriber (subjectKey) {
            var list = $scope.$parent.targets;
            if (list.length > 0) {
                for (var i = 0, size = list.length; i < size; i++) {
                    //console.log("checking equals ", list[i], subjectKey);
                    if (angular.equals(list[i].modelId, subjectKey)) {
                        return true;
                    }
                }
            }
            return false;
        }
    });

<|endoftext|>


<commit_before>var app = angular.module('SearchApp', []);

app.controller('SearchController', ['$scope', '$rootScope', '$http', '$window', function ($scope, $rootScope, $http, $window) {
    $scope.query = "";
    $scope.courses = [];
    $scope.teachers = [];
    $scope.loadingCourses = false;
    $scope.loadingTeachers = false;
    $scope.search = function () {
        $scope.courses = [];
        $scope.teachers = [];

        $scope.loadingTeachers = true;
        $scope.loadingCourses = true;

        $http.get('teacher/search', {
                params: {query: $scope.query}
            })
            .then(function (response) {
                $scope.teachers = response.data;
                $scope.loadingTeachers = false;
            });
        $http.get('course/search', {
                params: {query: $scope.query}
            })
            .then(function (response) {
                $scope.courses = response.data;
                $scope.loadingCourses = false;
            });
    };
    $scope.showTeacher = function (id) {
        $window.location.href = 'teacher/show?id=' + id;
    };
    $scope.showCourse = function (id) {
        $window.location.href = 'course/show?id=' + id;
    };
}]);

app.config(['$locationProvider', function ($locationProvider) {
    $locationProvider.html5Mode({enabled: true, rewriteLinks: false});
}]);<commit_msg>Automatic search like Google.<commit_after>var app = angular.module('SearchApp', []);

app.controller('SearchController', ['$scope', '$rootScope', '$http', '$window', function ($scope, $rootScope, $http, $window) {
    $scope.query = "";
    $scope.courses = [];
    $scope.teachers = [];
    $scope.loadingCourses = false;
    $scope.loadingTeachers = false;
    $scope.search = function () {
        $scope.courses = [];
        $scope.teachers = [];

        $scope.loadingTeachers = true;
        $scope.loadingCourses = true;

        $http.get('teacher/search', {
                params: {query: $scope.query}
            })
            .then(function (response) {
                $scope.teachers = response.data;
                $scope.loadingTeachers = false;
            });
        $http.get('course/search', {
                params: {query: $scope.query}
            })
            .then(function (response) {
                $scope.courses = response.data;
                $scope.loadingCourses = false;
            });
    };
    $scope.showTeacher = function (id) {
        $window.location.href = 'teacher/show?id=' + id;
    };
    $scope.showCourse = function (id) {
        $window.location.href = 'course/show?id=' + id;
    };

    $scope.$watch('query', function () {
        if ($scope.query.length < 3) {
            $scope.courses = [];
            $scope.teachers = [];
        } else {
            $scope.search();
        }
    });
}]);

app.config(['$locationProvider', function ($locationProvider) {
    $locationProvider.html5Mode({enabled: true, rewriteLinks: false});
}]);<|endoftext|>


<commit_before>({
  renderIcon: function(component) {
    var prefix = "slds-";
    var svgns = "http://www.w3.org/2000/svg";
    var xlinkns = "http://www.w3.org/1999/xlink";
    var size = component.get("v.size");
    var name = component.get("v.name");
    var classname = component.get("v.class");
    var category = component.get("v.category");

    var containerClassName = [
        prefix+"icon__container",
        prefix+"icon-"+category+"-"+name,
        classname
        ].join(' ');
    var iconClassName = prefix+"icon "+prefix+"icon--" + size;
    component.set("v.containerClass", containerClassName);

    var svgroot = document.createElementNS(svgns, "svg");
    svgroot.setAttribute("aria-hidden", "true");
    svgroot.setAttribute("class", iconClassName);
    svgroot.setAttribute("name", name);

    // Add an "href" attribute (using the "xlink" namespace)
    var shape = document.createElementNS(svgns, "use");
    shape.setAttributeNS(xlinkns, "href", component.get("v.svgPath"));
    svgroot.appendChild(shape);

    var container = component.find("container").getElement();
    container.insertBefore(svgroot, container.firstChild);

    var svgPath = component.get("v.svgPath");
    var namespace = "";
    //This would work with any class. We are just using the one with the shortest name.
    var gettingnamespace = sforce.connection.query("SELECT NamespacePrefix FROM ApexClass where Name = 'REL_Utils' LIMIT 1");
    var getname = gettingnamespace.getArray("records");
	if(getname.length > 0 && getname[0] && getname[0].length > 0) {
		namespace = getname[0].NamespacePrefix + "__";
		svgPath.replace('/resource/', '/resource/' + namespace);
		console.log("svgPath after manipulation: " + svgPath);
	}
    component.set("v.svgPath", svgPath);
  }
})<commit_msg>Oops. Js replace doesn't modify the original string.<commit_after>({
  renderIcon: function(component) {
    var prefix = "slds-";
    var svgns = "http://www.w3.org/2000/svg";
    var xlinkns = "http://www.w3.org/1999/xlink";
    var size = component.get("v.size");
    var name = component.get("v.name");
    var classname = component.get("v.class");
    var category = component.get("v.category");

    var containerClassName = [
        prefix+"icon__container",
        prefix+"icon-"+category+"-"+name,
        classname
        ].join(' ');
    var iconClassName = prefix+"icon "+prefix+"icon--" + size;
    component.set("v.containerClass", containerClassName);

    var svgroot = document.createElementNS(svgns, "svg");
    svgroot.setAttribute("aria-hidden", "true");
    svgroot.setAttribute("class", iconClassName);
    svgroot.setAttribute("name", name);

    // Add an "href" attribute (using the "xlink" namespace)
    var shape = document.createElementNS(svgns, "use");
    shape.setAttributeNS(xlinkns, "href", component.get("v.svgPath"));
    svgroot.appendChild(shape);

    var container = component.find("container").getElement();
    container.insertBefore(svgroot, container.firstChild);

    var svgPath = component.get("v.svgPath");
    var namespace = "";
    //This would work with any class. We are just using the one with the shortest name.
    var gettingnamespace = sforce.connection.query("SELECT NamespacePrefix FROM ApexClass where Name = 'REL_Utils' LIMIT 1");
    var getname = gettingnamespace.getArray("records");
	if(getname.length > 0 && getname[0] && getname[0].length > 0) {
		namespace = getname[0].NamespacePrefix + "__";
		svgPath = svgPath.replace('/resource/', '/resource/' + namespace);
		console.log("svgPath after manipulation: " + svgPath);
	}
    component.set("v.svgPath", svgPath);
  }
})<|endoftext|>