<commit_before>/*jshint eqeqeq:false */

var set = Ember.set, get = Ember.get, getPath = Ember.getPath;
var indexOf = Ember.EnumerableUtils.indexOf, indexesOf = Ember.EnumerableUtils.indexesOf;

/**
  @class

  The Ember.Select view class renders a
  [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element,
  allowing the user to choose from a list of options. The selected option(s)
  are updated live in the `selection` property.

  @extends Ember.View
*/
Ember.Select = Ember.View.extend(
  /** @scope Ember.Select.prototype */ {

  tagName: 'select',
  classNames: ['ember-select'],
  defaultTemplate: Ember.Handlebars.compile('{{#if view.prompt}}<option>{{view.prompt}}</option>{{/if}}{{#each view.content}}{{view Ember.SelectOption contentBinding="this"}}{{/each}}'),
  attributeBindings: ['multiple'],

  /**
    The `multiple` attribute of the select element. Indicates whether multiple
    options can be selected.

    @type Boolean
    @default false
  */
  multiple: false,

  /**
    The list of options.

    If `optionLabelPath` and `optionValuePath` are not overridden, this should
    be a list of strings, which will serve simultaneously as labels and values.

    Otherwise, this should be a list of objects. For instance:

        content: Ember.A([
            { id: 1, firstName: 'Yehuda' },
            { id: 2, firstName: 'Tom' }
          ])),
        optionLabelPath: 'content.firstName',
        optionValuePath: 'content.id'

    @type Array
    @default null
  */
  content: null,

  /**
    When `multiple` is false, the element of `content` that is currently
    selected, if any.

    When `multiple` is true, an array of such elements.

    @type Object or Array
    @default null
  */
  selection: null,

  /**
    In single selection mode (when `multiple` is false), value can be used to get
    the current selection's value or set the selection by it's value.

    It is not currently supported in multiple selection mode.

    @type String
    @default null
  */
  value: Ember.computed(function(key, value) {
    if (arguments.length === 2) { return value; }

    var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, '');
    return valuePath ? getPath(this, 'selection.' + valuePath) : get(this, 'selection');
  }).property('selection').cacheable(),

  /**
    If given, a top-most dummy option will be rendered to serve as a user
    prompt.

    @type String
    @default null
  */
  prompt: null,

  /**
    The path of the option labels. See `content`.

    @type String
    @default 'content'
  */
  optionLabelPath: 'content',

  /**
    The path of the option values. See `content`.

    @type String
    @default 'content'
  */
  optionValuePath: 'content',

  _change: function() {
    if (get(this, 'multiple')) {
      this._changeMultiple();
    } else {
      this._changeSingle();
    }
  },

  selectionDidChange: Ember.observer(function() {
    var selection = get(this, 'selection'),
        isArray = Ember.isArray(selection);
    if (get(this, 'multiple')) {
      if (!isArray) {
        set(this, 'selection', Ember.A([selection]));
        return;
      }
      this._selectionDidChangeMultiple();
    } else {
      this._selectionDidChangeSingle();
    }
  }, 'selection'),

  valueDidChange: Ember.observer(function() {
    var content = get(this, 'content'),
        value = get(this, 'value'),
        valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''),
        selectedValue = (valuePath ? getPath(this, 'selection.' + valuePath) : get(this, 'selection')),
        selection;

    if (value !== selectedValue) {
      selection = content.find(function(obj) {
        return value === (valuePath ? get(obj, valuePath) : obj);
      });

      this.set('selection', selection);
    }
  }, 'value'),

  _triggerChange: function() {
    var selection = get(this, 'selection');

    if (selection) { this.selectionDidChange(); }

    this._change();
  },

  _changeSingle: function() {
    var selectedIndex = this.$()[0].selectedIndex,
        content = get(this, 'content'),
        prompt = get(this, 'prompt');

    if (!content) { return; }
    if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; }

    if (prompt) { selectedIndex -= 1; }
    set(this, 'selection', content.objectAt(selectedIndex));
  },

  _changeMultiple: function() {
    var options = this.$('option:selected'),
        prompt = get(this, 'prompt'),
        offset = prompt ? 1 : 0,
        content = get(this, 'content');

    if (!content){ return; }
    if (options) {
      var selectedIndexes = options.map(function(){
        return this.index - offset;
      }).toArray();
      set(this, 'selection', content.objectsAt(selectedIndexes));
    }
  },

  _selectionDidChangeSingle: function() {
    var el = this.$()[0],
        content = get(this, 'content'),
        selection = get(this, 'selection'),
        selectionIndex = content ? indexOf(content, selection) : -1,
        prompt = get(this, 'prompt');

    if (prompt) { selectionIndex += 1; }
    if (el) { el.selectedIndex = selectionIndex; }
  },

  _selectionDidChangeMultiple: function() {
    var content = get(this, 'content'),
        selection = get(this, 'selection'),
        selectedIndexes = content ? indexesOf(content, selection) : [-1],
        prompt = get(this, 'prompt'),
        offset = prompt ? 1 : 0,
        options = this.$('option'),
        adjusted;

    if (options) {
      options.each(function() {
        adjusted = this.index > -1 ? this.index + offset : -1;
        this.selected = indexOf(selectedIndexes, adjusted) > -1;
      });
    }
  },

  init: function() {
    this._super();
    this.on("didInsertElement", this, this._triggerChange);
    this.on("change", this, this._change);
  }
});

Ember.SelectOption = Ember.View.extend({
  tagName: 'option',
  attributeBindings: ['value', 'selected'],

  defaultTemplate: function(context, options) {
    options = { data: options.data, hash: {} };
    Ember.Handlebars.helpers.bind.call(context, "view.label", options);
  },

  init: function() {
    this.labelPathDidChange();
    this.valuePathDidChange();

    this._super();
  },

  selected: Ember.computed(function() {
    var content = get(this, 'content'),
        selection = getPath(this, 'parentView.selection');
    if (getPath(this, 'parentView.multiple')) {
      return selection && indexOf(selection, content) > -1;
    } else {
      // Primitives get passed through bindings as objects... since
      // `new Number(4) !== 4`, we use `==` below
      return content == selection;
    }
  }).property('content', 'parentView.selection').volatile(),

  labelPathDidChange: Ember.observer(function() {
    var labelPath = getPath(this, 'parentView.optionLabelPath');

    if (!labelPath) { return; }

    Ember.defineProperty(this, 'label', Ember.computed(function() {
      return getPath(this, labelPath);
    }).property(labelPath).cacheable());
  }, 'parentView.optionLabelPath'),

  valuePathDidChange: Ember.observer(function() {
    var valuePath = getPath(this, 'parentView.optionValuePath');

    if (!valuePath) { return; }

    Ember.defineProperty(this, 'value', Ember.computed(function() {
      return getPath(this, valuePath);
    }).property(valuePath).cacheable());
  }, 'parentView.optionValuePath')
});

<commit_msg>Give Ember.Select prompt an empty value<commit_after>/*jshint eqeqeq:false */

var set = Ember.set, get = Ember.get, getPath = Ember.getPath;
var indexOf = Ember.EnumerableUtils.indexOf, indexesOf = Ember.EnumerableUtils.indexesOf;

/**
  @class

  The Ember.Select view class renders a
  [select](https://developer.mozilla.org/en/HTML/Element/select) HTML element,
  allowing the user to choose from a list of options. The selected option(s)
  are updated live in the `selection` property.

  @extends Ember.View
*/
Ember.Select = Ember.View.extend(
  /** @scope Ember.Select.prototype */ {

  tagName: 'select',
  classNames: ['ember-select'],
  defaultTemplate: Ember.Handlebars.compile('{{#if view.prompt}}<option value>{{view.prompt}}</option>{{/if}}{{#each view.content}}{{view Ember.SelectOption contentBinding="this"}}{{/each}}'),
  attributeBindings: ['multiple'],

  /**
    The `multiple` attribute of the select element. Indicates whether multiple
    options can be selected.

    @type Boolean
    @default false
  */
  multiple: false,

  /**
    The list of options.

    If `optionLabelPath` and `optionValuePath` are not overridden, this should
    be a list of strings, which will serve simultaneously as labels and values.

    Otherwise, this should be a list of objects. For instance:

        content: Ember.A([
            { id: 1, firstName: 'Yehuda' },
            { id: 2, firstName: 'Tom' }
          ])),
        optionLabelPath: 'content.firstName',
        optionValuePath: 'content.id'

    @type Array
    @default null
  */
  content: null,

  /**
    When `multiple` is false, the element of `content` that is currently
    selected, if any.

    When `multiple` is true, an array of such elements.

    @type Object or Array
    @default null
  */
  selection: null,

  /**
    In single selection mode (when `multiple` is false), value can be used to get
    the current selection's value or set the selection by it's value.

    It is not currently supported in multiple selection mode.

    @type String
    @default null
  */
  value: Ember.computed(function(key, value) {
    if (arguments.length === 2) { return value; }

    var valuePath = get(this, 'optionValuePath').replace(/^content\.?/, '');
    return valuePath ? getPath(this, 'selection.' + valuePath) : get(this, 'selection');
  }).property('selection').cacheable(),

  /**
    If given, a top-most dummy option will be rendered to serve as a user
    prompt.

    @type String
    @default null
  */
  prompt: null,

  /**
    The path of the option labels. See `content`.

    @type String
    @default 'content'
  */
  optionLabelPath: 'content',

  /**
    The path of the option values. See `content`.

    @type String
    @default 'content'
  */
  optionValuePath: 'content',

  _change: function() {
    if (get(this, 'multiple')) {
      this._changeMultiple();
    } else {
      this._changeSingle();
    }
  },

  selectionDidChange: Ember.observer(function() {
    var selection = get(this, 'selection'),
        isArray = Ember.isArray(selection);
    if (get(this, 'multiple')) {
      if (!isArray) {
        set(this, 'selection', Ember.A([selection]));
        return;
      }
      this._selectionDidChangeMultiple();
    } else {
      this._selectionDidChangeSingle();
    }
  }, 'selection'),

  valueDidChange: Ember.observer(function() {
    var content = get(this, 'content'),
        value = get(this, 'value'),
        valuePath = get(this, 'optionValuePath').replace(/^content\.?/, ''),
        selectedValue = (valuePath ? getPath(this, 'selection.' + valuePath) : get(this, 'selection')),
        selection;

    if (value !== selectedValue) {
      selection = content.find(function(obj) {
        return value === (valuePath ? get(obj, valuePath) : obj);
      });

      this.set('selection', selection);
    }
  }, 'value'),

  _triggerChange: function() {
    var selection = get(this, 'selection');

    if (selection) { this.selectionDidChange(); }

    this._change();
  },

  _changeSingle: function() {
    var selectedIndex = this.$()[0].selectedIndex,
        content = get(this, 'content'),
        prompt = get(this, 'prompt');

    if (!content) { return; }
    if (prompt && selectedIndex === 0) { set(this, 'selection', null); return; }

    if (prompt) { selectedIndex -= 1; }
    set(this, 'selection', content.objectAt(selectedIndex));
  },

  _changeMultiple: function() {
    var options = this.$('option:selected'),
        prompt = get(this, 'prompt'),
        offset = prompt ? 1 : 0,
        content = get(this, 'content');

    if (!content){ return; }
    if (options) {
      var selectedIndexes = options.map(function(){
        return this.index - offset;
      }).toArray();
      set(this, 'selection', content.objectsAt(selectedIndexes));
    }
  },

  _selectionDidChangeSingle: function() {
    var el = this.$()[0],
        content = get(this, 'content'),
        selection = get(this, 'selection'),
        selectionIndex = content ? indexOf(content, selection) : -1,
        prompt = get(this, 'prompt');

    if (prompt) { selectionIndex += 1; }
    if (el) { el.selectedIndex = selectionIndex; }
  },

  _selectionDidChangeMultiple: function() {
    var content = get(this, 'content'),
        selection = get(this, 'selection'),
        selectedIndexes = content ? indexesOf(content, selection) : [-1],
        prompt = get(this, 'prompt'),
        offset = prompt ? 1 : 0,
        options = this.$('option'),
        adjusted;

    if (options) {
      options.each(function() {
        adjusted = this.index > -1 ? this.index + offset : -1;
        this.selected = indexOf(selectedIndexes, adjusted) > -1;
      });
    }
  },

  init: function() {
    this._super();
    this.on("didInsertElement", this, this._triggerChange);
    this.on("change", this, this._change);
  }
});

Ember.SelectOption = Ember.View.extend({
  tagName: 'option',
  attributeBindings: ['value', 'selected'],

  defaultTemplate: function(context, options) {
    options = { data: options.data, hash: {} };
    Ember.Handlebars.helpers.bind.call(context, "view.label", options);
  },

  init: function() {
    this.labelPathDidChange();
    this.valuePathDidChange();

    this._super();
  },

  selected: Ember.computed(function() {
    var content = get(this, 'content'),
        selection = getPath(this, 'parentView.selection');
    if (getPath(this, 'parentView.multiple')) {
      return selection && indexOf(selection, content) > -1;
    } else {
      // Primitives get passed through bindings as objects... since
      // `new Number(4) !== 4`, we use `==` below
      return content == selection;
    }
  }).property('content', 'parentView.selection').volatile(),

  labelPathDidChange: Ember.observer(function() {
    var labelPath = getPath(this, 'parentView.optionLabelPath');

    if (!labelPath) { return; }

    Ember.defineProperty(this, 'label', Ember.computed(function() {
      return getPath(this, labelPath);
    }).property(labelPath).cacheable());
  }, 'parentView.optionLabelPath'),

  valuePathDidChange: Ember.observer(function() {
    var valuePath = getPath(this, 'parentView.optionValuePath');

    if (!valuePath) { return; }

    Ember.defineProperty(this, 'value', Ember.computed(function() {
      return getPath(this, valuePath);
    }).property(valuePath).cacheable());
  }, 'parentView.optionValuePath')
});

<|endoftext|>


<commit_before>// ==========================================================================
// Project:   Ember - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
//            Portions ©2008-2011 Apple Inc. All rights reserved.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

require('ember-views/views/view');
var get = Ember.get, set = Ember.set, meta = Ember.meta;

var childViewsProperty = Ember.computed(function() {
  return get(this, '_childViews');
}).property('_childViews').cacheable();

Ember.ContainerView = Ember.View.extend({

  init: function() {
    var childViews = get(this, 'childViews');
    Ember.defineProperty(this, 'childViews', childViewsProperty);

    this._super();

    var _childViews = get(this, '_childViews');

    childViews.forEach(function(viewName, idx) {
      var view;

      if ('string' === typeof viewName) {
        view = get(this, viewName);
        view = this.createChildView(view);
        set(this, viewName, view);
      } else {
        view = this.createChildView(viewName);
      }

      _childViews[idx] = view;
    }, this);
  },

  /**
    Extends Ember.View's implementation of renderToBuffer to
    set up an array observer on the child views array. This
    observer will detect when child views are added or removed
    and update the DOM to reflect the mutation.

    Note that we set up this array observer in the `renderToBuffer`
    method because any views set up previously will be rendered the first
    time the container is rendered.

    @private
  */
  renderToBuffer: function() {
    var ret = this._super.apply(this, arguments);

    get(this, 'childViews').addArrayObserver(this, {
      willChange: 'childViewsWillChange',
      didChange: 'childViewsDidChange'
    });

    return ret;
  },

  /**
    Instructs each child view to render to the passed render buffer.

    @param {Ember.RenderBuffer} buffer the buffer to render to
    @private
  */
  render: function(buffer) {
    this.forEachChildView(function(view) {
      view.renderToBuffer(buffer);
    });
  },

  /**
    When the container view is destroyer, tear down the child views
    array observer.

    @private
  */
  destroy: function() {
    get(this, 'childViews').removeArrayObserver(this, {
      willChange: 'childViewsWillChange',
      didChange: 'childViewsDidChange'
    });

    this._super();
  },

  /**
    When a child view is removed, destroy its element so that
    it is removed from the DOM.

    The array observer that triggers this action is set up in the
    `renderToBuffer` method.

    @private
    @param {Ember.Array} views the child views array before mutation
    @param {Number} start the start position of the mutation
    @param {Number} removed the number of child views removed
  **/
  childViewsWillChange: function(views, start, removed) {
    if (removed === 0) { return; }

    var changedViews = views.slice(start, removed);
    this.setParentView(changedViews, null);

    this.invokeForState('childViewsWillChange', views, start, removed);
  },

  /**
    When a child view is added, make sure the DOM gets updated appropriately.

    If the view has already rendered an element, we tell the child view to
    create an element and insert it into the DOM. If the enclosing container view
    has already written to a buffer, but not yet converted that buffer into an
    element, we insert the string representation of the child into the appropriate
    place in the buffer.

    @private
    @param {Ember.Array} views the array of child views afte the mutation has occurred
    @param {Number} start the start position of the mutation
    @param {Number} removed the number of child views removed
    @param {Number} the number of child views added
  */
  childViewsDidChange: function(views, start, removed, added) {
    var len = get(views, 'length');

    // No new child views were added; bail out.
    if (added === 0) return;

    var changedViews = views.slice(start, added);
    this.setParentView(changedViews, this);

    // Let the current state handle the changes
    this.invokeForState('childViewsDidChange', views, start, added);
  },

  setParentView: function(views, parentView) {
    views.forEach(function(view) {
      set(view, '_parentView', parentView);
    });
  },

  /**
    Schedules a child view to be inserted into the DOM after bindings have
    finished syncing for this run loop.

    @param {Ember.View} view the child view to insert
    @param {Ember.View} prev the child view after which the specified view should
                     be inserted
    @private
  */
  _scheduleInsertion: function(view, prev) {
    if (prev) {
      prev.get('domManager').after(view);
    } else {
      this.get('domManager').prepend(view);
    }
  }
});

// Ember.ContainerView extends the default view states to provide different
// behavior for childViewsWillChange and childViewsDidChange.
Ember.ContainerView.states = {
  parent: Ember.View.states,

  inBuffer: {
    childViewsDidChange: function(parentView, views, start, added) {
      var buffer = meta(parentView)['Ember.View'].buffer,
          startWith, prev, prevBuffer, view;

      // Determine where to begin inserting the child view(s) in the
      // render buffer.
      if (start === 0) {
        // If views were inserted at the beginning, prepend the first
        // view to the render buffer, then begin inserting any
        // additional views at the beginning.
        view = views[start];
        startWith = start + 1;
        view.renderToBuffer(buffer, 'prepend');
      } else {
        // Otherwise, just insert them at the same place as the child
        // views mutation.
        view = views[start - 1];
        startWith = start;
      }

      for (var i=startWith; i<start+added; i++) {
        prev = view;
        view = views[i];
        prevBuffer = meta(prev)['Ember.View'].buffer;
        view.renderToBuffer(prevBuffer, 'insertAfter');
      }
    }
  },

  hasElement: {
    childViewsWillChange: function(view, views, start, removed) {
      for (var i=start; i<start+removed; i++) {
        views[i].destroyElement();
      }
    },

    childViewsDidChange: function(view, views, start, added) {
      // If the DOM element for this container view already exists,
      // schedule each child view to insert its DOM representation after
      // bindings have finished syncing.
      var prev = start === 0 ? null : views[start-1];

      for (var i=start; i<start+added; i++) {
        view = views[i];
        this._scheduleInsertion(view, prev);
        prev = view;
      }
    }
  }
};

Ember.ContainerView.states.inDOM = {
  parentState: Ember.ContainerView.states.hasElement
};

Ember.ContainerView.reopen({
  states: Ember.ContainerView.states
});
<commit_msg>Fix typo.<commit_after>// ==========================================================================
// Project:   Ember - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
//            Portions ©2008-2011 Apple Inc. All rights reserved.
// License:   Licensed under MIT license (see license.js)
// ==========================================================================

require('ember-views/views/view');
var get = Ember.get, set = Ember.set, meta = Ember.meta;

var childViewsProperty = Ember.computed(function() {
  return get(this, '_childViews');
}).property('_childViews').cacheable();

Ember.ContainerView = Ember.View.extend({

  init: function() {
    var childViews = get(this, 'childViews');
    Ember.defineProperty(this, 'childViews', childViewsProperty);

    this._super();

    var _childViews = get(this, '_childViews');

    childViews.forEach(function(viewName, idx) {
      var view;

      if ('string' === typeof viewName) {
        view = get(this, viewName);
        view = this.createChildView(view);
        set(this, viewName, view);
      } else {
        view = this.createChildView(viewName);
      }

      _childViews[idx] = view;
    }, this);
  },

  /**
    Extends Ember.View's implementation of renderToBuffer to
    set up an array observer on the child views array. This
    observer will detect when child views are added or removed
    and update the DOM to reflect the mutation.

    Note that we set up this array observer in the `renderToBuffer`
    method because any views set up previously will be rendered the first
    time the container is rendered.

    @private
  */
  renderToBuffer: function() {
    var ret = this._super.apply(this, arguments);

    get(this, 'childViews').addArrayObserver(this, {
      willChange: 'childViewsWillChange',
      didChange: 'childViewsDidChange'
    });

    return ret;
  },

  /**
    Instructs each child view to render to the passed render buffer.

    @param {Ember.RenderBuffer} buffer the buffer to render to
    @private
  */
  render: function(buffer) {
    this.forEachChildView(function(view) {
      view.renderToBuffer(buffer);
    });
  },

  /**
    When the container view is destroyed, tear down the child views
    array observer.

    @private
  */
  destroy: function() {
    get(this, 'childViews').removeArrayObserver(this, {
      willChange: 'childViewsWillChange',
      didChange: 'childViewsDidChange'
    });

    this._super();
  },

  /**
    When a child view is removed, destroy its element so that
    it is removed from the DOM.

    The array observer that triggers this action is set up in the
    `renderToBuffer` method.

    @private
    @param {Ember.Array} views the child views array before mutation
    @param {Number} start the start position of the mutation
    @param {Number} removed the number of child views removed
  **/
  childViewsWillChange: function(views, start, removed) {
    if (removed === 0) { return; }

    var changedViews = views.slice(start, removed);
    this.setParentView(changedViews, null);

    this.invokeForState('childViewsWillChange', views, start, removed);
  },

  /**
    When a child view is added, make sure the DOM gets updated appropriately.

    If the view has already rendered an element, we tell the child view to
    create an element and insert it into the DOM. If the enclosing container view
    has already written to a buffer, but not yet converted that buffer into an
    element, we insert the string representation of the child into the appropriate
    place in the buffer.

    @private
    @param {Ember.Array} views the array of child views afte the mutation has occurred
    @param {Number} start the start position of the mutation
    @param {Number} removed the number of child views removed
    @param {Number} the number of child views added
  */
  childViewsDidChange: function(views, start, removed, added) {
    var len = get(views, 'length');

    // No new child views were added; bail out.
    if (added === 0) return;

    var changedViews = views.slice(start, added);
    this.setParentView(changedViews, this);

    // Let the current state handle the changes
    this.invokeForState('childViewsDidChange', views, start, added);
  },

  setParentView: function(views, parentView) {
    views.forEach(function(view) {
      set(view, '_parentView', parentView);
    });
  },

  /**
    Schedules a child view to be inserted into the DOM after bindings have
    finished syncing for this run loop.

    @param {Ember.View} view the child view to insert
    @param {Ember.View} prev the child view after which the specified view should
                     be inserted
    @private
  */
  _scheduleInsertion: function(view, prev) {
    if (prev) {
      prev.get('domManager').after(view);
    } else {
      this.get('domManager').prepend(view);
    }
  }
});

// Ember.ContainerView extends the default view states to provide different
// behavior for childViewsWillChange and childViewsDidChange.
Ember.ContainerView.states = {
  parent: Ember.View.states,

  inBuffer: {
    childViewsDidChange: function(parentView, views, start, added) {
      var buffer = meta(parentView)['Ember.View'].buffer,
          startWith, prev, prevBuffer, view;

      // Determine where to begin inserting the child view(s) in the
      // render buffer.
      if (start === 0) {
        // If views were inserted at the beginning, prepend the first
        // view to the render buffer, then begin inserting any
        // additional views at the beginning.
        view = views[start];
        startWith = start + 1;
        view.renderToBuffer(buffer, 'prepend');
      } else {
        // Otherwise, just insert them at the same place as the child
        // views mutation.
        view = views[start - 1];
        startWith = start;
      }

      for (var i=startWith; i<start+added; i++) {
        prev = view;
        view = views[i];
        prevBuffer = meta(prev)['Ember.View'].buffer;
        view.renderToBuffer(prevBuffer, 'insertAfter');
      }
    }
  },

  hasElement: {
    childViewsWillChange: function(view, views, start, removed) {
      for (var i=start; i<start+removed; i++) {
        views[i].destroyElement();
      }
    },

    childViewsDidChange: function(view, views, start, added) {
      // If the DOM element for this container view already exists,
      // schedule each child view to insert its DOM representation after
      // bindings have finished syncing.
      var prev = start === 0 ? null : views[start-1];

      for (var i=start; i<start+added; i++) {
        view = views[i];
        this._scheduleInsertion(view, prev);
        prev = view;
      }
    }
  }
};

Ember.ContainerView.states.inDOM = {
  parentState: Ember.ContainerView.states.hasElement
};

Ember.ContainerView.reopen({
  states: Ember.ContainerView.states
});
<|endoftext|>


<commit_before>/*   Riassence Framework
 *   Copyright 2006 Riassence Inc.
 *   http://riassence.com/
 *
 *   You should have received a copy of the GNU General Public License along
 *   with this software package. If not, contact licensing@riassence.com
 */

/*** = Description
  ** HSlider is a control unit that enables the user to choose a value in a range of values.
  ** Sliders support both dragging the handle and clicking the mouse anywhere on the slider
  ** to move the handle towards the mouse, as well as keyboard support
  ** after the handle is in active mode. There are two types of sliders: vertical and horizontal.
  ** Naturally, sliders are commonly used as colour mixers, volume controls,
  ** graphical equalizers and seekers in media applications.
  ** A typical slider is a drag-able thumb along vertical or horizontal line.
  ** Slider view or theme can be changed; the helmiTheme is used by default.
  **
  ** = Instance variables
  ** +value+::      Numeric value currently set to this object.
  ** +minValue+::   The minimum value that can be set to this object.
  ** +maxValue::    The maximum value that can be set to this object.
  **
***/
HSlider = HControl.extend({

  componentName: "slider",

  controlDefaults: (HControlDefaults.extend({
      // The smallest allowed value
      minValue: 0,

      // The biggest allowed value
      maxValue: 1,

      // Interval in milliseconds for repeat
      repeatDelay: 300,

      // Interval in milliseconds for repeat
      repeatInterval: 50,

      // Inverse Scrollwheel axis:
      // As there is only one scrollwheel event, sideways
      // scrolling doesn't work logically for horizonal
      // scrollbars by default, so set this to true to
      // have horizonal sliders work logically
      // with sideways scrolling, where supported.
      inverseAxis: false
  })),

/** = Description
  * Like the +HControl.constructor+, except:
  * Sets the default event responders to:
  * - +mouseDown+: +false+
  * - +mouseup+: +false+
  * - +draggable+: +true+
  * - +keyDown+: +true+
  * - +keyUp+: +true+
  * - +mouseWheel+: +true+
  * Uses the following extra attributes to +_options+:
  * +minValue+::  The smallest value the slider can set. Defaults to 0.
  *
  * +maxValue+::  The largest value the slider can set. Defaults to 1.
  *
  * +value+::     The the initial position of the slider. Defaults to 0.
  *
  * +repatDelay+::  The key repetition initial delay when changing the slider
  *                 with cursor keys. Defaults to 300 (ms)
  *
  * +repeatInterval+::  The key repetition interval when changing the slider
  *                     with cursor keys. Defaults to 50 (ms)
  *
  * +inverseAxis+::  Inverts the axis of the slider. Defaults to false.
  *
  **/
  constructor: function(_rect,_parent,_options) {

    // Makes sure there is at least an empty options block
    if (!_options) {
      _options = {};
    }

    // Makes sure the default events for HStepper are enabled
    if (!_options.events) {
      _options.events = {
        mouseDown: false,
        mouseUp:   false,
        draggable: true,
        keyDown: true,
        keyUp: true,
        mouseWheel: true
      };
    }

    if(this.isinherited){
      this.base(_rect,_parent,_options);
    }
    else {
      this.isinherited = true;
      this.base(_rect,_parent,_options);
      this.isinherited = false;
    }

    this.refreshOnValueChange = false;

    // This is overridden in vertical slider.
    this._isVertical = false;

    if(!this.isinherited){
      this.draw();
    }

  },

/** = Description
  * Sets the current value of the object and moves the slider thumb to the correct position.
  *
  * = Parameters
  * +_value+::  A numeric value to be set to the object.
  *
  **/
  setValue: function(_value) {
    if (_value < this.minValue) {
      _value = this.minValue;
    }
    if (_value > this.maxValue) {
      _value = this.maxValue;
    }
    this.base(_value);
    if(this._thumbElemId){
      this.drawThumbPos();
    }
    return this;
  },

/** = Description
  * Draws the rectangle and the markup of this object on the screen.
  *
  **/
  draw: function() {
    if(!this.drawn) {
      this.drawRect();
      this.drawMarkup();
      this._initThumb();
    }
    this.refresh();
  },

/** = Description
  * This gets called automatically when the user starts to drag the slider thumb.
  * Extend this method if you want something special to happen when the dragging starts.
  *
  * = Parameters
  * +_x+::  The X coordinate of the point where the drag started.
  * +_y+::  The Y coordinate of the point where the drag started.
  *
  **/
  startDrag: function(_x,_y){
    var _originalPosition = ELEM.getVisiblePosition(this.elemId, true);
    this._originX = _originalPosition[0];
    this._originY = _originalPosition[1];

    this.doDrag(_x,_y);
  },

/** = Description
  * This gets called automatically when the user stops dragging the slider thumb.
  * Extend this method if you want something special to happen when the dragging ends.
  *
  * = Parameters
  * +_x+::  The X coordinate of the point where the drag ended.
  * +_y+::  The Y coordinate of the point where the drag ended.
  *
  **/
  endDrag: function(_x,_y){
    this.doDrag(_x,_y);
  },

/** = Description
  * This gets called periodically while the user drags the slider thumb.
  * Extend this method if you want something special to happen while dragging.
  *
  * = Parameters
  * +_x+::  The X coordinate of the point where the user is currently dragging.
  * +_y+::  The Y coordinate of the point where the user is currently dragging.
  *
  **/
  doDrag: function(_x,_y){
    _x -= this._originX;
    _y -= this._originY;

    var _rawVal = this._isVertical?_y:_x;
    var _value = this._pos2value(_rawVal);
    this.setValue(_value);
  },

/** = Description
  * This gets called when the user presses a key down while this control is
  * active. The default behaviour is to move the thumb with arrow keys, page up,
  * page down, home and end.
  *
  * = Parameters
  * +_keycode+::  The keycode of the key that was pressed down.
  *
  **/
  keyDown: function(_keycode) {
    // Arrow keys move the thumb 5% at a time.
    if ( (_keycode === Event.KEY_LEFT && !this._isVertical) ||
      (_keycode === Event.KEY_DOWN && this._isVertical) ) {
      this._moving = true;
      this._moveThumb(-0.05);
    }
    else if ( (_keycode === Event.KEY_RIGHT && !this._isVertical) ||
      (_keycode === Event.KEY_UP && this._isVertical) ) {
      this._moving = true;
      this._moveThumb(0.05);
    }
    // Home key moves the thumb to the beginning and end key to the end.
    else if (_keycode === Event.KEY_HOME) {
      this.setValue(this.minValue);
    }
    else if (_keycode === Event.KEY_END) {
      this.setValue(this.maxValue);
    }
    // Page up and page down keys move the thumb 25% at a time.
    else if (_keycode === Event.KEY_PAGEDOWN) {
      this._moving = true;
      this._moveThumb(-0.25);
    }
    else if (_keycode === Event.KEY_PAGEUP) {
      this._moving = true;
      this._moveThumb(0.25);
    }

  },

/** = Description
  * This gets called when the user releases a key while this control is active.
  *
  * = Parameters
  * +_keycode+::  The keycode of the key that was released.
  *
  **/
  keyUp: function(_keycode) {
    this._moving = false;
  },

/** = Description
  * This gets called when the mouse wheel is used and the component
  * instance has focus.
  *
  * = Parameters
  * +_delta+::  Scrolling delta, the wheel angle change. If delta is positive,
  *             wheel was scrolled up. Otherwise, it was scrolled down.
  *
  **/
  mouseWheel: function(_delta) {
    var _valueChange;
    if (_delta > 0) {
      _valueChange = 0.05;
    }
    else {
      _valueChange = -0.05;
    }
    if ( this.options.inverseAxis ) {
      _valueChange = 0 - _valueChange;
    }

    var _value = (this.maxValue - this.minValue) * _valueChange;
    this.setValue( this.value + _value);
  },

  // --private method++
  _moveThumb: function(_valueChange, _rate) {

    if (!_rate) {
      // --If the key is held down, wait for a while before starting repeat.++
      _rate = this.options.repeatDelay;
    }
    else if (_rate === this.options.repeatDelay) {
      _rate = this.options.repeatInterval;
    }

    if (this._moving && this.active) {

      var _value = (this.maxValue - this.minValue) * _valueChange;

      this.setValue( this.value + _value);

      var _that = this;
      if (this._thumbMoveTimeout) {
        window.clearTimeout(this._thumbMoveTimeout);
        this._thumbMoveTimeout = null;
      }
      this._thumbMoveTimeout = window.setTimeout(function(){
        _that._moveThumb(_valueChange, _rate);
      }, _rate);
    }

  },

  thumbSize: 21,
  // -- private method ++
  _initThumb: function() {
    this._thumbElemId = this.markupElemIds.control;
    this.drawThumbPos();
  },

  // -- private method ++
  _value2px: function() {
    var _pxrange;
    if(this._isVertical){
      _pxrange  = this.rect.height - this.thumbSize;
    } else {
      _pxrange  = this.rect.width - this.thumbSize;
    }
    var _intvalue = _pxrange * (
      (this.value-this.minValue) / (this.maxValue - this.minValue)
    );
    if ( this._isVertical ) {
      _intvalue = _pxrange - _intvalue;
    }
    _pxvalue = parseInt(_intvalue, 10)+'px';
    return _pxvalue;
  },

  // -- private method ++
  _pos2value: function(_mousePos) {
    var _pxrange;
    if(this._isVertical){
      _pxrange  = this.rect.height - this.thumbSize;
    } else {
      _pxrange  = this.rect.width - this.thumbSize;
    }
    _mousePos -= (this.thumbSize/2);
    if(_mousePos < 0){
      _mousePos = 0;
    }
    if(_mousePos > _pxrange){
      _mousePos = _pxrange;
    }
    if(this._isVertical){
      return this.maxValue - ((_mousePos / _pxrange) * (this.maxValue - this.minValue));
    } else {
      return this.minValue + ((_mousePos / _pxrange) * (this.maxValue - this.minValue));
    }
  },

  // -- private method ++
  drawThumbPos: function() {
    var _whichprop = this._isVertical?'top':'left',
        _propval   = this._value2px();
    ELEM.setStyle(this._thumbElemId,_whichprop,_propval);
    this.setOrientation(this.options['orientation']||this.prevOrientation);
  },

  prevOrientation: 'c',

  cssClassPrefix: 'h',

/** = Description
  * setOrientation function
  *
  * = Parameters
  * +_orientation+::
  *
  **/
  setOrientation: function(_orientation) {
    if(!_orientation){
      _orientation = 'c';
    }
    _orientation = _orientation.toLowerCase();
    if(_orientation === this.prevOrientation){
      return;
    }
    if(this['markupElemIds']===undefined){
      return;
    }
    if(this.markupElemIds['control']===undefined){
      return;
    }
    var _toggleCSS = this.toggleCSSClass,
        _ctrlId    = this.markupElemIds.control,
        _orientations = ['n','s','w','e','c'],
        _iOrientation = '',
        _cssClassName = '',
        _cssClassPrefix = this.cssClassPrefix,
        _cssClassVert = this._isVertical?'v':'',
        _activeOrientation = false,
        i = 0;
    for(;i<5;i++){
      _iOrientation = _orientations[i];
      _activeOrientation = (_orientation===_iOrientation);
      _cssClassName = (_orientation==='c')?_cssClassPrefix+_cssClassVert+'slider_thumb':_cssClassPrefix+'slider_thumb_'+_iOrientation;
      _toggleCSS( _ctrlId, _cssClassName, _activeOrientation );
    }

    this.prevOrientation = _orientation;
  }
});

<commit_msg>Documentation update<commit_after>/*   Riassence Framework
 *   Copyright 2006 Riassence Inc.
 *   http://riassence.com/
 *
 *   You should have received a copy of the GNU General Public License along
 *   with this software package. If not, contact licensing@riassence.com
 */

/*** = Description
  ** HSlider is a control unit that enables the user to choose a value in a range of values.
  ** Sliders support both dragging the handle and clicking the mouse anywhere on the slider
  ** to move the handle towards the mouse, as well as keyboard support
  ** after the handle is in active mode. There are two types of sliders: vertical and horizontal.
  ** Naturally, sliders are commonly used as colour mixers, volume controls,
  ** graphical equalizers and seekers in media applications.
  ** A typical slider is a drag-able thumb along vertical or horizontal line.
  ** Slider view or theme can be changed; the helmiTheme is used by default.
  **
  ** = Instance variables
  ** +value+:: Numeric value currently set to this object.
  ** +minValue+:: The minimum value that can be set to this object.
  ** +maxValue:: The maximum value that can be set to this object.
  **
  ** = Instance variables
  ** +minValue+:: The smallest allowed value.
  ** +maxValue+:: The biggest allowed value.
  ** +repeatDelay+:: The key repetition initial delay when changing the slider
  **                 with cursor keys. Defaults to 300 (ms)
  ** +repeatInterval+:: The key repetition interval when changing the slider
  **                    with cursor keys. Defaults to 50 (ms)
  ** +inveseAxis+:: Inverse Scrollwheel axis.
  **                As there is only one scrollwheel event, sideways
  **                scrolling doesn't work logically for horizonal
  **                scrollbars by default, so set this to true to
  **                have horizonal sliders work logically
  **                with sideways scrolling, where supported.
***/
HSlider = HControl.extend({

  componentName: "slider",

  controlDefaults: (HControlDefaults.extend({
      minValue: 0,
      maxValue: 1,
      repeatDelay: 300,
      repeatInterval: 50,
      inverseAxis: false
  })),

/** = Description
  * Use like the HControl#constructor.
  * Defaults:
  * +mouseDown+:: +false+
  * +mouseup+:: +false+
  * +draggable+:: +true+
  * +keyDown+:: +true+
  * +keyUp+:: +true+
  * +mouseWheel+:: +true+
  * +value+:: +0+
  *
  **/
  constructor: function(_rect,_parent,_options) {

    if (!_options) {
      _options = {};
    }

    if (!_options.events) {
      _options.events = {
        mouseDown: false,
        mouseUp:   false,
        draggable: true,
        keyDown: true,
        keyUp: true,
        mouseWheel: true
      };
    }

    if(this.isinherited){
      this.base(_rect,_parent,_options);
    }
    else {
      this.isinherited = true;
      this.base(_rect,_parent,_options);
      this.isinherited = false;
    }

    this.refreshOnValueChange = false;

    this._isVertical = false;

    if(!this.isinherited){
      this.draw();
    }

  },

/** = Description
  * Sets the current value of the object and moves the slider thumb to the correct position.
  *
  * = Parameters
  * +_value+:: A numeric value to be set to the object.
  *
  **/
  setValue: function(_value) {
    if (_value < this.minValue) {
      _value = this.minValue;
    }
    if (_value > this.maxValue) {
      _value = this.maxValue;
    }
    this.base(_value);
    if(this._thumbElemId){
      this.drawThumbPos();
    }
    return this;
  },

/** = Description
  * Draws the rectangle and the markup of this object on the screen.
  *
  **/
  draw: function() {
    if(!this.drawn) {
      this.drawRect();
      this.drawMarkup();
      this._initThumb();
    }
    this.refresh();
  },

/** = Description
  * This gets called automatically when the user starts to drag the slider thumb.
  * Extend this method if you want something special to happen when the dragging starts.
  *
  * = Parameters
  * +_x+:: The X coordinate of the point where the drag started.
  * +_y+:: The Y coordinate of the point where the drag started.
  *
  **/
  startDrag: function(_x,_y){
    var _originalPosition = ELEM.getVisiblePosition(this.elemId, true);
    this._originX = _originalPosition[0];
    this._originY = _originalPosition[1];

    this.doDrag(_x,_y);
  },

/** = Description
  * This gets called automatically when the user stops dragging the slider thumb.
  * Extend this method if you want something special to happen when the dragging ends.
  *
  * = Parameters
  * +_x+:: The X coordinate of the point where the drag ended.
  * +_y+:: The Y coordinate of the point where the drag ended.
  *
  **/
  endDrag: function(_x,_y){
    this.doDrag(_x,_y);
  },

/** = Description
  * This gets called periodically while the user drags the slider thumb.
  * Extend this method if you want something special to happen while dragging.
  *
  * = Parameters
  * +_x+:: The X coordinate of the point where the user is currently dragging.
  * +_y+:: The Y coordinate of the point where the user is currently dragging.
  *
  **/
  doDrag: function(_x,_y){
    _x -= this._originX;
    _y -= this._originY;

    var _rawVal = this._isVertical?_y:_x;
    var _value = this._pos2value(_rawVal);
    this.setValue(_value);
  },

/** = Description
  * This gets called when the user presses a key down while this control is
  * active. The default behaviour is to move the thumb with arrow keys, page up,
  * page down, home and end.
  *
  * = Parameters
  * +_keycode+:: The keycode of the key that was pressed down.
  *
  **/
  keyDown: function(_keycode) {
    // Arrow keys move the thumb 5% at a time.
    if ( (_keycode === Event.KEY_LEFT && !this._isVertical) ||
      (_keycode === Event.KEY_DOWN && this._isVertical) ) {
      this._moving = true;
      this._moveThumb(-0.05);
    }
    else if ( (_keycode === Event.KEY_RIGHT && !this._isVertical) ||
      (_keycode === Event.KEY_UP && this._isVertical) ) {
      this._moving = true;
      this._moveThumb(0.05);
    }
    // Home key moves the thumb to the beginning and end key to the end.
    else if (_keycode === Event.KEY_HOME) {
      this.setValue(this.minValue);
    }
    else if (_keycode === Event.KEY_END) {
      this.setValue(this.maxValue);
    }
    // Page up and page down keys move the thumb 25% at a time.
    else if (_keycode === Event.KEY_PAGEDOWN) {
      this._moving = true;
      this._moveThumb(-0.25);
    }
    else if (_keycode === Event.KEY_PAGEUP) {
      this._moving = true;
      this._moveThumb(0.25);
    }

  },

/** = Description
  * This gets called when the user releases a key while this control is active.
  *
  * = Parameters
  * +_keycode+:: The keycode of the key that was released.
  *
  **/
  keyUp: function(_keycode) {
    this._moving = false;
  },

/** = Description
  * This gets called when the mouse wheel is used and the component
  * instance has focus.
  *
  * = Parameters
  * +_delta+:: Scrolling delta, the wheel angle change. If delta is positive,
  *            wheel was scrolled up. Otherwise, it was scrolled down.
  *
  **/
  mouseWheel: function(_delta) {
    var _valueChange;
    if (_delta > 0) {
      _valueChange = 0.05;
    }
    else {
      _valueChange = -0.05;
    }
    if ( this.options.inverseAxis ) {
      _valueChange = 0 - _valueChange;
    }

    var _value = (this.maxValue - this.minValue) * _valueChange;
    this.setValue( this.value + _value);
  },

  // --private method++
  _moveThumb: function(_valueChange, _rate) {

    if (!_rate) {
      // --If the key is held down, wait for a while before starting repeat.++
      _rate = this.options.repeatDelay;
    }
    else if (_rate === this.options.repeatDelay) {
      _rate = this.options.repeatInterval;
    }

    if (this._moving && this.active) {

      var _value = (this.maxValue - this.minValue) * _valueChange;

      this.setValue( this.value + _value);

      var _that = this;
      if (this._thumbMoveTimeout) {
        window.clearTimeout(this._thumbMoveTimeout);
        this._thumbMoveTimeout = null;
      }
      this._thumbMoveTimeout = window.setTimeout(function(){
        _that._moveThumb(_valueChange, _rate);
      }, _rate);
    }

  },

  thumbSize: 21,
  // -- private method ++
  _initThumb: function() {
    this._thumbElemId = this.markupElemIds.control;
    this.drawThumbPos();
  },

  // -- private method ++
  _value2px: function() {
    var _pxrange;
    if(this._isVertical){
      _pxrange  = this.rect.height - this.thumbSize;
    } else {
      _pxrange  = this.rect.width - this.thumbSize;
    }
    var _intvalue = _pxrange * (
      (this.value-this.minValue) / (this.maxValue - this.minValue)
    );
    if ( this._isVertical ) {
      _intvalue = _pxrange - _intvalue;
    }
    _pxvalue = parseInt(_intvalue, 10)+'px';
    return _pxvalue;
  },

  // -- private method ++
  _pos2value: function(_mousePos) {
    var _pxrange;
    if(this._isVertical){
      _pxrange  = this.rect.height - this.thumbSize;
    } else {
      _pxrange  = this.rect.width - this.thumbSize;
    }
    _mousePos -= (this.thumbSize/2);
    if(_mousePos < 0){
      _mousePos = 0;
    }
    if(_mousePos > _pxrange){
      _mousePos = _pxrange;
    }
    if(this._isVertical){
      return this.maxValue - ((_mousePos / _pxrange) * (this.maxValue - this.minValue));
    } else {
      return this.minValue + ((_mousePos / _pxrange) * (this.maxValue - this.minValue));
    }
  },

  // -- private method ++
  drawThumbPos: function() {
    var _whichprop = this._isVertical?'top':'left',
        _propval   = this._value2px();
    ELEM.setStyle(this._thumbElemId,_whichprop,_propval);
    this.setOrientation(this.options['orientation']||this.prevOrientation);
  },

  prevOrientation: 'c',

  cssClassPrefix: 'h',

/** = Description
  * Changes the thumb graphic. Possible orientations by default are
  * north ('n'), south ('s'), west ('w'), east('e') and center ('c').
  * Defaults to 'c'. No case sensitivity.
  *
  * = Parameters
  * +_orientation+:: The orientation of slider thumb graphic.
  *
  **/
  setOrientation: function(_orientation) {
    if(!_orientation){
      _orientation = 'c';
    }
    _orientation = _orientation.toLowerCase();
    if(_orientation === this.prevOrientation){
      return;
    }
    if(this['markupElemIds']===undefined){
      return;
    }
    if(this.markupElemIds['control']===undefined){
      return;
    }
    var _toggleCSS = this.toggleCSSClass,
        _ctrlId    = this.markupElemIds.control,
        _orientations = ['n','s','w','e','c'],
        _iOrientation = '',
        _cssClassName = '',
        _cssClassPrefix = this.cssClassPrefix,
        _cssClassVert = this._isVertical?'v':'',
        _activeOrientation = false,
        i = 0;
    for(;i<5;i++){
      _iOrientation = _orientations[i];
      _activeOrientation = (_orientation===_iOrientation);
      _cssClassName = (_orientation==='c')?_cssClassPrefix+_cssClassVert+'slider_thumb':_cssClassPrefix+'slider_thumb_'+_iOrientation;
      _toggleCSS( _ctrlId, _cssClassName, _activeOrientation );
    }

    this.prevOrientation = _orientation;
  }
});

<|endoftext|>


<commit_before>//----------------------------------------------------------------------------
//  Copyright (C) 2013 The IPython Development Team
//
//  Distributed under the terms of the BSD License.  The full license is in
//  the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------

//============================================================================
// MultiContainerWidget
//============================================================================

/**
 * @module IPython
 * @namespace IPython
 **/

define(["notebook/js/widget"], function(widget_manager){
    var MulticontainerModel = IPython.WidgetModel.extend({});
    widget_manager.register_widget_model('MulticontainerWidgetModel', MulticontainerModel);

    var AccordionView = IPython.WidgetView.extend({

        render: function(){
            var guid = 'accordion' + IPython.utils.uuid();
            this.$el = $('<div />', {id: guid})
                .addClass('accordion');
            this._ensureElement();
            this.containers = [];
        },

        update: function() {
            // Set tab titles
            var titles = this.model.get('_titles');
            for (var page_index in titles) {

                var accordian = this.containers[page_index]
                if (accordian != undefined) {
                    accordian
                        .find('.accordion-heading')
                        .find('.accordion-toggle')
                        .html(titles[page_index]);
                }
            }

            // Set selected page
            var selected_index = this.model.get("selected_index");
            if (0 <= selected_index && selected_index < this.containers.length) {
                for (var index in this.containers) {
                    if (index==selected_index) {
                        this.containers[index].find('.accordion-body').collapse('show');
                    } else {
                        this.containers[index].find('.accordion-body').collapse('hide');
                    }

                }
            }

            return IPython.WidgetView.prototype.update.call(this);
        },

        display_child: function(view) {

            var index = this.containers.length;
            var uuid = IPython.utils.uuid();
            var accordion_group = $('<div />')
                .addClass('accordion-group')
                .appendTo(this.$el);
            var accordion_heading = $('<div />')
                .addClass('accordion-heading')
                .appendTo(accordion_group);
            var that = this;
            var accordion_toggle = $('<a />')
                .addClass('accordion-toggle')
                .attr('data-toggle', 'collapse')
                .attr('data-parent', '#' + this.$el.attr('id'))
                .attr('href', '#' + uuid)
                .click(function(evt){
                    that.model.set("selected_index", index);
                    that.model.update_other_views(that);
                 })
                .html('Page ' + index)
                .appendTo(accordion_heading);
            var accordion_body = $('<div />', {id: uuid})
                .addClass('accordion-body collapse')
                .appendTo(accordion_group);
            var accordion_inner = $('<div />')
                .addClass('accordion-inner')
                .appendTo(accordion_body);
            this.containers.push(accordion_group);
            accordion_inner.append(view.$el);

            this.update();

            // Stupid workaround to close the bootstrap accordion tabs which
            // open by default even though they don't have the `in` class
            // attached to them.  For some reason a delay is required.
            // TODO: Better fix.
            setTimeout(function(){that.update()}, 500);
        },
    });

    widget_manager.register_widget_view('AccordionView', AccordionView);

    var TabView = IPython.WidgetView.extend({

        render: function(){
            var uuid = 'tabs'+IPython.utils.uuid();
            var that = this;
            this.$tabs = $('<div />', {id: uuid})
                .addClass('nav')
                .addClass('nav-tabs')
                .appendTo(this.$el);
            this.$tab_contents = $('<div />', {id: uuid + 'Content'})
                .addClass('tab-content')
                .appendTo(this.$el);

            this.containers = [];
        },

        update: function() {
            // Set tab titles
            var titles = this.model.get('_titles');
            for (var page_index in titles) {
                var tab_text = this.containers[page_index]
                if (tab_text != undefined) {
                    tab_text.html(titles[page_index]);
                }
            }

            var selected_index = this.model.get('selected_index');
            if (0 <= selected_index && selected_index < this.containers.length) {
                this.select_page(selected_index);
            }

            return IPython.WidgetView.prototype.update.call(this);
        },

        display_child: function(view) {

            var index = this.containers.length;
            var uuid = IPython.utils.uuid();

            var that = this;
            var tab = $('<li />')
                .css('list-style-type', 'none')
                .appendTo(this.$tabs);
            var tab_text = $('<a />')
                .attr('href', '#' + uuid)
                .attr('data-toggle', 'tab')
                .html('Page ' + index)
                .appendTo(tab)
                .click(function (e) {
                    that.model.set("selected_index", index);
                    that.model.update_other_views(that);
                    that.select_page(index);
                });
            this.containers.push(tab_text);

            var contents_div = $('<div />', {id: uuid})
                .addClass('tab-pane')
                .addClass('fade')
                .append(view.$el)
                .appendTo(this.$tab_contents);

            if (index==0) {
                tab_text.tab('show');
            }
            this.update();
        },

        select_page: function(index) {
            this.$tabs.find('li')
                .removeClass('active');
            this.containers[index].tab('show');
        },
    });

    widget_manager.register_widget_view('TabView', TabView);
});
<commit_msg>Fixed backbone event handling for accordion view<commit_after>//----------------------------------------------------------------------------
//  Copyright (C) 2013 The IPython Development Team
//
//  Distributed under the terms of the BSD License.  The full license is in
//  the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------

//============================================================================
// MultiContainerWidget
//============================================================================

/**
 * @module IPython
 * @namespace IPython
 **/

define(["notebook/js/widget"], function(widget_manager){
    var MulticontainerModel = IPython.WidgetModel.extend({});
    widget_manager.register_widget_model('MulticontainerWidgetModel', MulticontainerModel);

    var AccordionView = IPython.WidgetView.extend({

        render: function(){
            var guid = 'accordion' + IPython.utils.uuid();
            this.$el
                .attr('id', guid)
                .addClass('accordion');
            this.containers = [];
        },

        update: function() {
            // Set tab titles
            var titles = this.model.get('_titles');
            for (var page_index in titles) {

                var accordian = this.containers[page_index]
                if (accordian != undefined) {
                    accordian
                        .find('.accordion-heading')
                        .find('.accordion-toggle')
                        .html(titles[page_index]);
                }
            }

            // Set selected page
            var selected_index = this.model.get("selected_index");
            if (0 <= selected_index && selected_index < this.containers.length) {
                for (var index in this.containers) {
                    if (index==selected_index) {
                        this.containers[index].find('.accordion-body').collapse('show');
                    } else {
                        this.containers[index].find('.accordion-body').collapse('hide');
                    }

                }
            }

            return IPython.WidgetView.prototype.update.call(this);
        },

        display_child: function(view) {

            var index = this.containers.length;
            var uuid = IPython.utils.uuid();
            var accordion_group = $('<div />')
                .addClass('accordion-group')
                .appendTo(this.$el);
            var accordion_heading = $('<div />')
                .addClass('accordion-heading')
                .appendTo(accordion_group);
            var that = this;
            var accordion_toggle = $('<a />')
                .addClass('accordion-toggle')
                .attr('data-toggle', 'collapse')
                .attr('data-parent', '#' + this.$el.attr('id'))
                .attr('href', '#' + uuid)
                .click(function(evt){
                    that.model.set("selected_index", index);
                    that.model.update_other_views(that);
                 })
                .html('Page ' + index)
                .appendTo(accordion_heading);
            var accordion_body = $('<div />', {id: uuid})
                .addClass('accordion-body collapse')
                .appendTo(accordion_group);
            var accordion_inner = $('<div />')
                .addClass('accordion-inner')
                .appendTo(accordion_body);
            this.containers.push(accordion_group);
            accordion_inner.append(view.$el);

            this.update();

            // Stupid workaround to close the bootstrap accordion tabs which
            // open by default even though they don't have the `in` class
            // attached to them.  For some reason a delay is required.
            // TODO: Better fix.
            setTimeout(function(){that.update()}, 500);
        },
    });

    widget_manager.register_widget_view('AccordionView', AccordionView);

    var TabView = IPython.WidgetView.extend({

        render: function(){
            var uuid = 'tabs'+IPython.utils.uuid();
            var that = this;
            this.$tabs = $('<div />', {id: uuid})
                .addClass('nav')
                .addClass('nav-tabs')
                .appendTo(this.$el);
            this.$tab_contents = $('<div />', {id: uuid + 'Content'})
                .addClass('tab-content')
                .appendTo(this.$el);

            this.containers = [];
        },

        update: function() {
            // Set tab titles
            var titles = this.model.get('_titles');
            for (var page_index in titles) {
                var tab_text = this.containers[page_index]
                if (tab_text != undefined) {
                    tab_text.html(titles[page_index]);
                }
            }

            var selected_index = this.model.get('selected_index');
            if (0 <= selected_index && selected_index < this.containers.length) {
                this.select_page(selected_index);
            }

            return IPython.WidgetView.prototype.update.call(this);
        },

        display_child: function(view) {

            var index = this.containers.length;
            var uuid = IPython.utils.uuid();

            var that = this;
            var tab = $('<li />')
                .css('list-style-type', 'none')
                .appendTo(this.$tabs);
            var tab_text = $('<a />')
                .attr('href', '#' + uuid)
                .attr('data-toggle', 'tab')
                .html('Page ' + index)
                .appendTo(tab)
                .click(function (e) {
                    that.model.set("selected_index", index);
                    that.model.update_other_views(that);
                    that.select_page(index);
                });
            this.containers.push(tab_text);

            var contents_div = $('<div />', {id: uuid})
                .addClass('tab-pane')
                .addClass('fade')
                .append(view.$el)
                .appendTo(this.$tab_contents);

            if (index==0) {
                tab_text.tab('show');
            }
            this.update();
        },

        select_page: function(index) {
            this.$tabs.find('li')
                .removeClass('active');
            this.containers[index].tab('show');
        },
    });

    widget_manager.register_widget_view('TabView', TabView);
});
<|endoftext|>


<commit_before>import _ from 'lodash'
import {expect} from 'chai'
import {describeComponent, it} from 'ember-mocha'
import wait from 'ember-test-helpers/wait'
import hbs from 'htmlbars-inline-precompile'
import dummyData from './dummyInput'

const viewSchema = {
  low: {
    'version': '1.0',
    'type': 'form',
    'rootContainers': [
      {'label': 'Main', 'container': 'main'}
    ],
    'containers': [
      {
        'id': 'main',
        'className': 'flex-row',
        'rows': [
          [
            {'model': 'alias', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ],
          [
            {
              'model': 'updatedAt',
              'label': 'Last Updated',
              'labelClassName': 'ob-label',
              'inputClassName': 'ob-input'
            }
          ]
        ]
      }
    ]
  },
  medium: {
    'version': '1.0',
    'type': 'form',
    'rootContainers': [
      {'label': 'Main', 'container': 'main'}
    ],
    'containers': [
      {
        'id': 'main',
        'className': 'flex-row',
        'rows': [
          [
            {'model': 'alias', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'},
            {'model': 'value', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ],
          [
            {
              'model': 'updatedAt',
              'label': 'Last Updated',
              'labelClassName': 'ob-label',
              'inputClassName': 'ob-input'
            }
          ]
        ]
      }
    ]
  },
  high: {
    'version': '1.0',
    'type': 'form',
    'rootContainers': [
      {'label': 'Main', 'container': 'main'}
    ],
    'containers': [
      {
        'id': 'main',
        'className': 'flex-row',
        'rows': [
          [
            {'model': 'alias', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'},
            {'model': 'value', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ],
          [
            {
              'model': 'updatedAt',
              'label': 'Last Updated',
              'labelClassName': 'ob-label',
              'inputClassName': 'ob-input'
            },
            {'model': 'id', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ]
        ]
      }
    ]
  }
}

const actionBarItems = [
  {label: 'Details', id: 'details', enabled: false},
  {label: 'Delete', id: 'delete', enabled: false},
  {label: 'Edit', id: 'edit', enabled: false}
]

const resources = _.map(dummyData.resources, (resource) => {
  return Ember.Object.create(resource)
})

const model = {resources: Ember.A(resources), model: dummyData.model}

describeComponent('frost-object-browser', 'Integration | Component | frost object browser', {
  integration: true
}, function () {
  beforeEach(function () {
    this.setProperties({
      actionBarItems,
      model,
      viewSchema
    })
  })

  it('renders', function () {
    // takes too long to render the list
    this.timeout(5000)
    this.render(hbs`{{frost-object-browser
      actionBarItems=actionBarItems
      values=model.resources
      model=model.model
    }}`)
    expect(this.$()).to.have.length(1)
  })

  it('renders 6 items per page', function () {
    this.render(hbs`{{frost-object-browser
      actionBarItems=actionBarItems
      itemsPerPage=6
      values=model.resources
      model=model.model
    }}`)
    expect(this.$().find('.frost-list-item')).to.have.length(6)
  })

  it('it changes page when we click to next change button', function (done) {
    this.render(hbs`{{frost-object-browser
      actionBarItems=actionBarItems
      itemsPerPage=6
      values=model.resources
      model=model.model
    }}`)

    this.$().find('.pagination .button-bar.right button').eq(0).click()

    wait()
      .then(() => {
        expect(this.$().find('.pagination').text().trim()).to.equal('7 to 12 of 20')
        done()
      })
  })
})
<commit_msg>more waiting<commit_after>import _ from 'lodash'
import {expect} from 'chai'
import {describeComponent, it} from 'ember-mocha'
import wait from 'ember-test-helpers/wait'
import hbs from 'htmlbars-inline-precompile'
import dummyData from './dummyInput'

const viewSchema = {
  low: {
    'version': '1.0',
    'type': 'form',
    'rootContainers': [
      {'label': 'Main', 'container': 'main'}
    ],
    'containers': [
      {
        'id': 'main',
        'className': 'flex-row',
        'rows': [
          [
            {'model': 'alias', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ],
          [
            {
              'model': 'updatedAt',
              'label': 'Last Updated',
              'labelClassName': 'ob-label',
              'inputClassName': 'ob-input'
            }
          ]
        ]
      }
    ]
  },
  medium: {
    'version': '1.0',
    'type': 'form',
    'rootContainers': [
      {'label': 'Main', 'container': 'main'}
    ],
    'containers': [
      {
        'id': 'main',
        'className': 'flex-row',
        'rows': [
          [
            {'model': 'alias', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'},
            {'model': 'value', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ],
          [
            {
              'model': 'updatedAt',
              'label': 'Last Updated',
              'labelClassName': 'ob-label',
              'inputClassName': 'ob-input'
            }
          ]
        ]
      }
    ]
  },
  high: {
    'version': '1.0',
    'type': 'form',
    'rootContainers': [
      {'label': 'Main', 'container': 'main'}
    ],
    'containers': [
      {
        'id': 'main',
        'className': 'flex-row',
        'rows': [
          [
            {'model': 'alias', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'},
            {'model': 'value', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ],
          [
            {
              'model': 'updatedAt',
              'label': 'Last Updated',
              'labelClassName': 'ob-label',
              'inputClassName': 'ob-input'
            },
            {'model': 'id', 'labelClassName': 'ob-label', 'inputClassName': 'ob-input'}
          ]
        ]
      }
    ]
  }
}

const actionBarItems = [
  {label: 'Details', id: 'details', enabled: false},
  {label: 'Delete', id: 'delete', enabled: false},
  {label: 'Edit', id: 'edit', enabled: false}
]

const resources = _.map(dummyData.resources, (resource) => {
  return Ember.Object.create(resource)
})

const model = {resources: Ember.A(resources), model: dummyData.model}

describeComponent(
  'frost-object-browser',
  'Integration | Component | frost object browser',
  {
    integration: true
  },
  function () {
    beforeEach(function () {
      this.timeout(5000) // takes too long to render the list

      this.setProperties({
        actionBarItems,
        model,
        viewSchema
      })

      return wait()
    })

    afterEach(function () {
      this.timeout(2000)
    })

    it('renders', function () {
      this.render(hbs`{{frost-object-browser
        actionBarItems=actionBarItems
        values=model.resources
        model=model.model
      }}`)
      expect(this.$()).to.have.length(1)
    })

    it('renders 6 items per page', function () {
      this.render(hbs`{{frost-object-browser
        actionBarItems=actionBarItems
        itemsPerPage=6
        values=model.resources
        model=model.model
      }}`)
      expect(this.$().find('.frost-list-item')).to.have.length(6)
    })

    it('it changes page when we click to next change button', function () {
      this.render(hbs`{{frost-object-browser
        actionBarItems=actionBarItems
        itemsPerPage=6
        values=model.resources
        model=model.model
      }}`)

      this.$().find('.pagination .button-bar.right button').eq(0).click()

      return wait()
        .then(() => {
          expect(this.$().find('.pagination').text().trim()).to.equal('7 to 12 of 20')
        })
    })
  }
)
<|endoftext|>


<commit_before>"use strict";

/*
 * Screenshow Messages module
 * Author: Tuomas Virtanen
 */

function ScreenNowPlaying(settings, jmobj, obj, url) {
    this.settings = settings;
    this.obj = obj;
    this.url = url;
    this.jmobj = jmobj;

    this.timeout = 3000;
    this.cache = [];
    this.is_stopped = false;

    this.init = function() {}

    this.post_init = function() {
        this.obj.on('enterStep', $.proxy(this.start, this));
        this.obj.on('leaveStep', $.proxy(this.stop, this));
    }

    this.render = function(data) {
        // Write HTML from cache
        var output = '';
        var k = 0;
        $.each(this.cache, function(key, value) {
            if(k < 5 && value['state'] == 0) {
                if(k == 0) {
                    output += '
';
                } else {
                    output += '
';
                }
                output += value['artist'];
                if(value['artist'] != '' && value['title'] != '') {
                    output += ' · ';
                }
                output += value['title'];
                output += '
';
                k++;
            }
        });
        this.obj.html('
Toistossa:
' + output);
    }

    this.fetch_success = function(data) {
        // Save to cache
        this.cache = data['playlist'];
        this.render();

        // Set jmpress stuff
        if(this.cache.length > 0) {
            this.is_stopped = (this.cache[0]['state'] == 1);
        }
        this.obj.data("stepData").exclude = (this.cache.length == 0 || this.is_stopped);
        this.jmobj.jmpress('reapply', this.obj);
    }

    this.fetch_error = function(jqXHR, status, errorThrown) {
        console.log("There was a problem while fetching playlist data.");
        this.obj.data("stepData").exclude = (this.cache.length == 0);
    }

    this.update = function() {
        $.ajax({
            url: this.url,
            dataType: 'json',
            success: $.proxy(this.fetch_success, this),
            timeout: this.timeout,
            type: 'GET',
            error: $.proxy(this.fetch_error, this)
        });
    }
}
<commit_msg>screenshow: Add »<commit_after>"use strict";

/*
 * Screenshow Messages module
 * Author: Tuomas Virtanen
 */

function ScreenNowPlaying(settings, jmobj, obj, url) {
    this.settings = settings;
    this.obj = obj;
    this.url = url;
    this.jmobj = jmobj;

    this.timeout = 3000;
    this.cache = [];
    this.is_stopped = false;

    this.init = function() {}

    this.post_init = function() {
        this.obj.on('enterStep', $.proxy(this.start, this));
        this.obj.on('leaveStep', $.proxy(this.stop, this));
    }

    this.render = function(data) {
        // Write HTML from cache
        var output = '';
        var k = 0;
        $.each(this.cache, function(key, value) {
            if(k < 5 && value['state'] == 0) {
                if(k == 0) {
                    output += '
» ';
                } else {
                    output += '
';
                }
                output += value['artist'];
                if(value['artist'] != '' && value['title'] != '') {
                    output += ' · ';
                }
                output += value['title'];
                output += '
';
                k++;
            }
        });
        this.obj.html('
Toistossa:
' + output);
    }

    this.fetch_success = function(data) {
        // Save to cache
        this.cache = data['playlist'];
        this.render();

        // Set jmpress stuff
        if(this.cache.length > 0) {
            this.is_stopped = (this.cache[0]['state'] == 1);
        }
        this.obj.data("stepData").exclude = (this.cache.length == 0 || this.is_stopped);
        this.jmobj.jmpress('reapply', this.obj);
    }

    this.fetch_error = function(jqXHR, status, errorThrown) {
        console.log("There was a problem while fetching playlist data.");
        this.obj.data("stepData").exclude = (this.cache.length == 0);
    }

    this.update = function() {
        $.ajax({
            url: this.url,
            dataType: 'json',
            success: $.proxy(this.fetch_success, this),
            timeout: this.timeout,
            type: 'GET',
            error: $.proxy(this.fetch_error, this)
        });
    }
}
<|endoftext|>


<commit_before>/*$AMPERSAND_VERSION*/
var _ = require('underscore');
var Events = require('backbone-events-standalone');
var classExtend = require('ampersand-class-extend');
var underscoreMixins = require('ampersand-collection-underscore-mixin');
var slice = Array.prototype.slice;

function SubCollection(collection, spec) {
    this.reset();
    this.collection = collection;
    this._spec = (spec || (spec = this._spec));
    if (spec.comparator) this.comparator = spec.comparator;
    this._watch(spec.watched || []);
    this._parseFilters(spec);
    this._runFilters();
    this.listenTo(this.collection, 'all', this._onCollectionEvent);
}

_.extend(SubCollection.prototype, Events, underscoreMixins, {
    // add a filter function directly
    addFilter: function (filter) {
        this.swapFilters([filter], []);
    },

    // remove filter function directly
    removeFilter: function (filter) {
        this.swapFilters([], [filter]);
    },

    // clears filters fires events for changes
    clearFilters: function () {
        this._resetFilters();
        this._runFilters();
    },

    // Swap out a set of old filters with a set of
    // new filters
    swapFilters: function (newFilters, oldFilters) {
        var self = this;

        if (!oldFilters) {
            oldFilters = this._filters;
        } else if (!_.isArray(oldFilters)) {
            oldFilters = [oldFilters];
        }

        if (!newFilters) {
            newFilters = [];
        } else if (!_.isArray(newFilters)) {
            newFilters = [newFilters];
        }

        oldFilters.forEach(function (filter) {
            self._removeFilter(filter);
        });

        newFilters.forEach(function (filter) {
            self._addFilter(filter);
        });

        this._runFilters();
    },

    // Update sub collection config, if `clear`
    // then clear existing spec before start.
    // This takes all the same filter arguments
    // as the init function. So you can pass:
    // {
    //   where: {
    //      name: 'something'
    //   },
    //   limit: 20
    // }
    configure: function (opts, clear) {
        if (clear) this.reset();
        _.extend(this._spec, opts);
        if (opts.comparator) this.comparator = opts.comparator;
        if (opts.watched) this._watch(opts.watched);
        this._parseFilters(opts);
        this._runFilters();
    },

    // gets a model at a given index
    at: function (index) {
        return this.models[index];
    },

    // proxy `get` method to the underlying collection
    get: function (query, indexName) {
        var model = this.collection.get(query, indexName);
        if (model && this.contains(model)) return model;
    },

    // remove filter if found
    _removeFilter: function (filter) {
        var index = this._filters.indexOf(filter);
        if (index !== -1) {
            this._filters.splice(index, 1);
        }
    },

    // clear all filters, reset everything
    reset: function () {
        this._spec = {};
        this.models = [];
        this._watched = [];
        this.comparator = undefined;
        this._resetFilters();
    },

    // just reset filters, no model changes
    _resetFilters: function () {
        this._filters = [];
        this._unwatch(_.keys(this._spec.where));
        this.limit = undefined;
        this.offset = undefined;
    },

    // internal method registering new filter function
    _addFilter: function (filter) {
        this._filters.push(filter);
    },

    // adds a property or array of properties to watch, ensures uniquness.
    _watch: function (item) {
        this._watched = _.union(this._watched, _.isArray(item) ? item : [item]);
    },

    // removes a watched property
    _unwatch: function (item) {
        this._watched = _.without(this._watched, item);
        this._watched = _.difference(this._watched, _.isArray(item) ? item : [item]);
    },

    _parseFilters: function (spec) {
        if (spec.where) {
            _.each(spec.where, function (value, item) {
                this._addFilter(function (model) {
                    return (model.get ? model.get(item) : model[item]) === value;
                });
            }, this);
            // also make sure we watch all `where` keys
            this._watch(_.keys(spec.where));
        }
        if (spec.hasOwnProperty('limit')) this.limit = spec.limit;
        if (spec.hasOwnProperty('offset')) this.offset = spec.offset;
        if (spec.filter) {
            this._addFilter(spec.filter, false);
        }
        if (spec.filters) {
            spec.filters.forEach(this._addFilter, this);
        }
    },

    _runFilters: function () {
        // make a copy of the array for comparisons
        var existingModels = slice.call(this.models);
        var rootModels = slice.call(this.collection.models);
        var offset = (this.offset || 0);
        var newModels, toAdd, toRemove;

        // reduce base model set by applying filters
        if (this._filters.length) {
            newModels = _.reduce(this._filters, function (startingArray, filterFunc) {
                return startingArray.filter(filterFunc);
            }, rootModels);
        } else {
            newModels = slice.call(rootModels);
        }

        // sort it
        if (this.comparator) newModels = _.sortBy(newModels, this.comparator);

        // trim it to length
        if (this.limit || this.offset) newModels = newModels.slice(offset, this.limit + offset);

        // now we've got our new models time to compare
        toAdd = _.difference(newModels, existingModels);
        toRemove = _.difference(existingModels, newModels);

        // save 'em
        this.models = newModels;

        _.each(toRemove, function (model) {
            this.trigger('remove', model, this);
        }, this);

        _.each(toAdd, function (model) {
            this.trigger('add', model, this);
        }, this);

        // if they contain the same models, but in new order, trigger sort
        if (!_.isEqual(existingModels, newModels)) {
            this.trigger('sort', this);
        }
    },

    _onCollectionEvent: function (eventName, model) {
        var propName = eventName.split(':')[1];
        // conditions under which we should re-run filters
        if (propName === this.comparator || _.contains(this._watched, propName) || _.contains(['add', 'remove', 'reset', 'sync'], eventName)) {
            this._runFilters();
        }
        // conditions under which we should proxy the events
        if ((_.contains(['sync', 'invalid', 'destroy']) || eventName.indexOf('change') !== -1) && this.contains(model)) {
            this.trigger.apply(this, arguments);
        }
    }
});

Object.defineProperty(SubCollection.prototype, 'length', {
    get: function () {
        return this.models.length;
    }
});

Object.defineProperty(SubCollection.prototype, 'isCollection', {
    get: function () {
        return true;
    }
});

SubCollection.extend = classExtend;

module.exports = SubCollection;
<commit_msg>make _unwatch work like _watch<commit_after>/*$AMPERSAND_VERSION*/
var _ = require('underscore');
var Events = require('backbone-events-standalone');
var classExtend = require('ampersand-class-extend');
var underscoreMixins = require('ampersand-collection-underscore-mixin');
var slice = Array.prototype.slice;

function SubCollection(collection, spec) {
    this.reset();
    this.collection = collection;
    this._spec = (spec || (spec = this._spec));
    if (spec.comparator) this.comparator = spec.comparator;
    this._watch(spec.watched || []);
    this._parseFilters(spec);
    this._runFilters();
    this.listenTo(this.collection, 'all', this._onCollectionEvent);
}

_.extend(SubCollection.prototype, Events, underscoreMixins, {
    // add a filter function directly
    addFilter: function (filter) {
        this.swapFilters([filter], []);
    },

    // remove filter function directly
    removeFilter: function (filter) {
        this.swapFilters([], [filter]);
    },

    // clears filters fires events for changes
    clearFilters: function () {
        this._resetFilters();
        this._runFilters();
    },

    // Swap out a set of old filters with a set of
    // new filters
    swapFilters: function (newFilters, oldFilters) {
        var self = this;

        if (!oldFilters) {
            oldFilters = this._filters;
        } else if (!_.isArray(oldFilters)) {
            oldFilters = [oldFilters];
        }

        if (!newFilters) {
            newFilters = [];
        } else if (!_.isArray(newFilters)) {
            newFilters = [newFilters];
        }

        oldFilters.forEach(function (filter) {
            self._removeFilter(filter);
        });

        newFilters.forEach(function (filter) {
            self._addFilter(filter);
        });

        this._runFilters();
    },

    // Update sub collection config, if `clear`
    // then clear existing spec before start.
    // This takes all the same filter arguments
    // as the init function. So you can pass:
    // {
    //   where: {
    //      name: 'something'
    //   },
    //   limit: 20
    // }
    configure: function (opts, clear) {
        if (clear) this.reset();
        _.extend(this._spec, opts);
        if (opts.comparator) this.comparator = opts.comparator;
        if (opts.watched) this._watch(opts.watched);
        this._parseFilters(opts);
        this._runFilters();
    },

    // gets a model at a given index
    at: function (index) {
        return this.models[index];
    },

    // proxy `get` method to the underlying collection
    get: function (query, indexName) {
        var model = this.collection.get(query, indexName);
        if (model && this.contains(model)) return model;
    },

    // remove filter if found
    _removeFilter: function (filter) {
        var index = this._filters.indexOf(filter);
        if (index !== -1) {
            this._filters.splice(index, 1);
        }
    },

    // clear all filters, reset everything
    reset: function () {
        this._spec = {};
        this.models = [];
        this._watched = [];
        this.comparator = undefined;
        this._resetFilters();
    },

    // just reset filters, no model changes
    _resetFilters: function () {
        this._filters = [];
        this._unwatch(_.keys(this._spec.where));
        this.limit = undefined;
        this.offset = undefined;
    },

    // internal method registering new filter function
    _addFilter: function (filter) {
        this._filters.push(filter);
    },

    // adds a property or array of properties to watch, ensures uniquness.
    _watch: function (item) {
        this._watched = _.union(this._watched, _.isArray(item) ? item : [item]);
    },

    // removes a watched property
    _unwatch: function (item) {
        this._watched = _.difference(this._watched, _.isArray(item) ? item : [item]);
    },

    _parseFilters: function (spec) {
        if (spec.where) {
            _.each(spec.where, function (value, item) {
                this._addFilter(function (model) {
                    return (model.get ? model.get(item) : model[item]) === value;
                });
            }, this);
            // also make sure we watch all `where` keys
            this._watch(_.keys(spec.where));
        }
        if (spec.hasOwnProperty('limit')) this.limit = spec.limit;
        if (spec.hasOwnProperty('offset')) this.offset = spec.offset;
        if (spec.filter) {
            this._addFilter(spec.filter, false);
        }
        if (spec.filters) {
            spec.filters.forEach(this._addFilter, this);
        }
    },

    _runFilters: function () {
        // make a copy of the array for comparisons
        var existingModels = slice.call(this.models);
        var rootModels = slice.call(this.collection.models);
        var offset = (this.offset || 0);
        var newModels, toAdd, toRemove;

        // reduce base model set by applying filters
        if (this._filters.length) {
            newModels = _.reduce(this._filters, function (startingArray, filterFunc) {
                return startingArray.filter(filterFunc);
            }, rootModels);
        } else {
            newModels = slice.call(rootModels);
        }

        // sort it
        if (this.comparator) newModels = _.sortBy(newModels, this.comparator);

        // trim it to length
        if (this.limit || this.offset) newModels = newModels.slice(offset, this.limit + offset);

        // now we've got our new models time to compare
        toAdd = _.difference(newModels, existingModels);
        toRemove = _.difference(existingModels, newModels);

        // save 'em
        this.models = newModels;

        _.each(toRemove, function (model) {
            this.trigger('remove', model, this);
        }, this);

        _.each(toAdd, function (model) {
            this.trigger('add', model, this);
        }, this);

        // if they contain the same models, but in new order, trigger sort
        if (!_.isEqual(existingModels, newModels)) {
            this.trigger('sort', this);
        }
    },

    _onCollectionEvent: function (eventName, model) {
        var propName = eventName.split(':')[1];
        // conditions under which we should re-run filters
        if (propName === this.comparator || _.contains(this._watched, propName) || _.contains(['add', 'remove', 'reset', 'sync'], eventName)) {
            this._runFilters();
        }
        // conditions under which we should proxy the events
        if ((_.contains(['sync', 'invalid', 'destroy']) || eventName.indexOf('change') !== -1) && this.contains(model)) {
            this.trigger.apply(this, arguments);
        }
    }
});

Object.defineProperty(SubCollection.prototype, 'length', {
    get: function () {
        return this.models.length;
    }
});

Object.defineProperty(SubCollection.prototype, 'isCollection', {
    get: function () {
        return true;
    }
});

SubCollection.extend = classExtend;

module.exports = SubCollection;
<|endoftext|>