/**
* @module "montage/ui/repetition.reel"
*/
var Montage = require("../../core/core").Montage;
var Component = require("../component").Component;
var RangeController = require("../../core/range-controller").RangeController;
var Promise = require("../../core/promise").Promise;
var PressComposer = require("../../composer/press-composer").PressComposer;
var Map = require("collections/map");
var Set = require("collections/set");
var logger = require("../../core/logger").logger("repetition").color.magenta();
var TIMEOUT_BEFORE_ITERATION_BECOME_ACTIVE = 60;
/**
* A reusable view-model for each iteration of a repetition. Each iteration
* corresponds to a value from the {@link Repetition#contentController}.
* When an iteration is drawn, it is tied to the corresponding controller-model
* that carries which object the iteration is coupled to, and whether it is
* selected.
*
* @class Iteration
* @extends Montage
*/
var Iteration = exports.Iteration = Montage.specialize( /** @lends Iteration.prototype # */ {
/**
* The parent repetition component.
* @private
*/
repetition: {value: null},
/**
* The repetition gets iterations from its `contentController`. The
* controller is responsible for tracking which iterations are drawn and
* which are selected. The iteration view-model is attached to the
* controller view-model by this property. The `selected` and `object`
* properties are bound to the eponymous properties of the iteration
* controller.
* @private
*/
controller: {value: null},
/**
* The corresponding content for this iteration.
* @type {Object}
*/
_object: {
value: null
},
object: {
get: function () {
return this._object;
},
set: function (value) {
var selected;
if (this._object !== value) {
this._object = value;
selected = this.repetition.contentController.selection.indexOf(value) !== -1;
if (this._selected !== selected) {
this.selected = selected;
}
}
}
},
/**
* A `DocumentFragment`, donated by the repetition's `_iterationTemplate`
* née `innerTemplate` which contains the elements that the
* iteration owns when they are not on the document between the top and
* bottom boundaries.
* @private
*/
_fragment: {value: null},
/**
* @private
*/
_childComponents: {value: null},
/**
* The position of this iteration within the content controller, and within
* the document immediately after the repetition has drawn.
* @type {number}
*/
index: {value: null},
/**
* The position of this iteration on the document last time it was drawn,
* and its position within the `repetition.drawnIterations`.
* @private
*/
_drawnIndex: {value: null},
/**
* Whether this iteration should be highlighted. It might be highlighted
* because the user is touching it, or because it is under some other user
* cursor as in an autocomplete popdown where the arrow keys manipulate the
* active iteration.
* @type {boolean}
*/
_active: {
value: false
},
active: {
set: function (active) {
active = !!active;
if (this.active !== active) {
this._active = active;
if (this.repetition) {
if (active) {
this.repetition.activeIterations.add(this);
} else {
this.repetition.activeIterations.delete(this);
}
this._updateRepetitionDirtyClassIteration();
}
}
},
get: function () {
return this._active;
}
},
/**
* A flag that indicates that the "no-transition" CSS class should be added
* to every element in the iteration in the next draw, and promptly removed
* the draw thereafter.
* @private
*/
_noTransition: {value: null},
/**
* The document part created by instantiating the iteration template.
*/
_templateDocumentPart: {value: null},
/**
* Set when the contents of the iteration no longer match their template.
*/
isDirty: {value: false},
_selected: {
value: false
},
selected: {
get: function () {
return this._selected;
},
set: function (value) {
value = !!value;
if (this.object && this.repetition && this.repetition.contentController) {
if (value) {
this.repetition.contentController.selection.add(this.object);
} else {
this.repetition.contentController.selection.delete(this.object);
}
}
if (this._selected !== value) {
this._selected = value;
this._updateRepetitionDirtyClassIteration();
}
}
},
/**
* Creates the initial values of all instance state.
* @private
*/
constructor: {
value: function Iteration() {
if (logger.isDebug) {
logger.debug("Iteration:%s create iteration %O", Object.hash(this), this);
}
this.repetition = null;
this.controller = null;
this.object = null;
// An iteration can be "on" or "off" the document. When the
// iteration is added to a document, the "fragment" is depopulated
// and placed between "topBoundary" and "bottomBoundary" on the
// DOM. The repetition manages the boundary markers around each
// drawn index.
this._fragment = null;
this._childComponents = null;
// The position that this iteration occupies in the controller.
// This is updated synchronously in response to changes to
// repetition.iterations, which are in turn synchronized with
// controller.iterations. The drawnIndex tracks the index by the
// end of the next Repetition.draw.
this.index = null;
// The position that this iteration occupies in the repetition.
// This is updated whenever views are added or removed before it in
// the sequence, an operation of linear complexity but which is not
// onerous since there should be a managable, fixed-maximum number
// of drawn iterations.
this._drawnIndex = null;
// Changes to whether a user is touching the iteration are
// reflected by the "active" CSS class on each element in the
// iteration. This gets updated in the draw cycle, in response to
// operations that handlePropertyChange adds to the repetition draw
// cycle.
//Fixme: Need to find the purpose of this property, probably dead code.
this._noTransition = false;
// dispatch handlePropertyChange:
this.addOwnPropertyChangeListener("_noTransition", this);
this.addPathChangeListener(
"index.defined() && _childComponents.defined()",
this,
"handleComponentModelChange"
);
this.cachedFirstElement = null;
}
},
_timeoutBecomeActiveID: {
value: null
},
_shouldBecomeActive: {
value: false
},
shouldBecomeActive: {
set: function (bool) {
if (this._timeoutBecomeActiveID) {
clearTimeout(this._timeoutBecomeActiveID);
this._timeoutBecomeActiveID = null;
}
if (bool) {
var self = this;
this._shouldBecomeActive = true;
this._timeoutBecomeActiveID = setTimeout(function () {
if (self._shouldBecomeActive) {
self.active = true;
}
self._shouldBecomeActive = false;
}, TIMEOUT_BEFORE_ITERATION_BECOME_ACTIVE);
} else {
this._shouldBecomeActive = false;
}
},
get: function () {
return this._shouldBecomeActive;
}
},
/**
* Associates the iteration instance with a repetition.
* @private
*/
initWithRepetition: {
value: function (repetition) {
this.repetition = repetition;
return this;
}
},
/**
* Disassociates an iteration with its content and prepares it to be
* recycled on the repetition's list of free iterations. This function is
* called by handleOrganizedContentRangeChange when it recycles an
* iteration.
* @private
*/
recycle: {
value: function () {
this.index = null;
this.object = null;
// Adding the "no-transition" class ensures that the iteration will
// stop any transitions applied when the iteration was bound to
// other content. It has the side-effect of scheduling a draw, and
// in that draw scheduling another draw to remove the
// "no-transition" class.
this._noTransition = true;
}
},
/**
* Injects this iteration to the document between its top and bottom
* boundaries.
* @param {number} index The drawn index at which to place the iteration.
* @private
*/
injectIntoDocument: {
value: function (index) {
if (this._drawnIndex !== null) {
if (logger.isDebug) {
logger.debug("Iteration:%s retracting from index %s and injecting at %s",Object.hash(this),this._drawnIndex, index);
}
this.retractFromDocument();
} else {
if (logger.isDebug) {
logger.debug("Iteration:%s injecting at index %s",Object.hash(this),index);
}
}
var self = this;
var repetition = this.repetition;
var element = repetition.element;
var boundaries = repetition._boundaries;
// Add a new top boundary before the next iteration
var topBoundary = element.ownerDocument.createTextNode("");
var bottomBoundary = boundaries[index]; // previous
boundaries.splice(index, 0, topBoundary);
element.insertBefore(topBoundary, bottomBoundary);
// Inject the elements into the document
element.insertBefore(this._fragment, bottomBoundary);
repetition._drawnIterations.splice(index, 0, this);
repetition._updateDrawnIndexes(index);
repetition._addDirtyClassListIteration(this);
if (this._elementsWillBeAddedToMap) {
return;
}
// Once the child components have drawn once, and thus created all
// their elements, we can add them to the _iterationForElement map
var childComponentsLeftToDraw = this._childComponents.length;
var firstDraw = function (event) {
event.target.removeEventListener("firstDraw", firstDraw, false);
childComponentsLeftToDraw--;
if (!childComponentsLeftToDraw) {
self.forEachElement(function (element) {
repetition._iterationForElement.set(element, self);
});
}
};
// notify the components to wake up and smell the document
if (this._childComponents.length > 0) {
for (var i = 0; i < this._childComponents.length; i++) {
var childComponent = this._childComponents[i];
childComponent.addEventListener("firstDraw", firstDraw, false);
childComponent.needsDraw = true;
if (childComponent._completedFirstDraw === true) {
throw Error("Repetiton:%s child component %O has already drawn.", Object.hash(this), childComponent);
}
}
} else {
this.forEachElement(function (element) {
repetition._iterationForElement.set(element, self);
});
}
this._elementsWillBeAddedToMap = true;
}
},
_elementsWillBeAddedToMap: {
value: false
},
/**
* Retracts an iteration from the document, scooping its child nodes into
* its DOMFragment.
* @private
*/
retractFromDocument: {
value: function () {
if (logger.isDebug) {
logger.debug("Iteration:%s retractFromDocument drawnIndex: %s",Object.hash(this), this._drawnIndex);
}
var index = this._drawnIndex;
var repetition = this.repetition;
var element = repetition.element;
var topBoundary = repetition._boundaries[index];
var bottomBoundary = repetition._boundaries[index + 1];
// Remove the elements between the boundaries. Also remove the top
// boundary and adjust the boundaries array accordingly so future
// injections and retractions can find their corresponding
// boundaries.
repetition._boundaries.splice(index, 1);
var fragment = this._fragment;
var child = topBoundary.nextSibling;
while (child !== bottomBoundary) {
var next = child.nextSibling;
element.removeChild(child);
fragment.appendChild(child);
child = next;
}
element.removeChild(topBoundary);
this._drawnIndex = null;
repetition._drawnIterations.splice(index, 1);
repetition._updateDrawnIndexes(index);
}
},
/**
* This is a method that responds to changes (and the initial value of) the
* FRB expression `index.defined() && _childComponents.defined()`.
* @private
*/
handleComponentModelChange: {
value: function (onComponentModel) {
var childComponents = this._childComponents,
repetition = this.repetition,
i, length;
if (onComponentModel) {
for (i = 0, length = childComponents.length; i < length; i++) {
repetition.addChildComponent(childComponents[i]);
}
// the second condition protects against removing before adding in
// the initial state.
} else if (this._childComponents) {
for (i = 0, length = childComponents.length; i < length; i++) {
repetition.removeChildComponent(childComponents[i]);
}
}
}
},
/**
* Dispatched by the "active" and "selected" property change listeners to
* notify the repetition that these iterations need to have their CSS class
* lists updated.
* @private
*/
_updateRepetitionDirtyClassIteration: {
value: function () {
if (!this.repetition) {
return;
}
this.repetition._addDirtyClassListIteration(this);
this.repetition.needsDraw = true;
}
},
/**
* A utility method for applying changes to every element in this iteration
* if it is on the document. This may be safely called on a retracted
* iteration with no effect.
* @private
*/
forEachElement: {
value: function (callback, thisp) {
var repetition = this.repetition;
var index = this._drawnIndex;
// Short-circuit if the iteration is not on the document.
if (index === null || index === undefined) {
return;
}
for (
var child = repetition._boundaries[index];
child !== repetition._boundaries[index + 1];
child = child.nextSibling
) {
if (child.nodeType === 1) { // tags
callback.call(thisp, child);
}
}
}
},
/**
* The first tag node inside this iteration. This is an accessor. The
* accessor function searches for the first element every time
* it is accessed, to protect against changes to the structure within
* the iteration.
*
* The accessor stores its result in `cachedFirstElement`. If you are
* certain that the internal structure of the repetition is consistent and
* have accessed `firstElement` at least once before, you can take
* advantage of quick access to `cachedFirstElement`.
* @private
*/
firstElement: {
get: function () {
var repetition = this.repetition;
var index = this._drawnIndex;
// Short-circuit if the iteration is not on the document.
if (index === null || index === undefined) {
return;
}
for (
var child = repetition._boundaries[index];
child !== repetition._boundaries[index + 1];
child = child.nextSibling
) {
if (child.nodeType === 1) { // tags
this.cachedFirstElement = child;
return child;
}
}
}
},
isComponentTreeLoaded: {
value: function () {
return this._fragment !== null;
}
},
/**
* The most recent result of the `firstElement` accessor, useful for speed
* if you know that the internal structure of the iteration is static.
* @private
*/
cachedFirstElement: {
value: null
}
});
Iteration.prototype.handlePropertyChange = Iteration.prototype._updateRepetitionDirtyClassIteration;
// Here it is, what we have all been waiting for, the prototype of the hour.
// Give it up for the Repetition...
/**
* @class Repetition
* @classdesc A component that repeats its inner template for each value in
* @desc
* A component that manages copies of its inner template for each value in its
* content. The content is managed by a controller. The repetition will
* create a {@link RangeController} for the content if you provide a
* [content]{@link Repetition#content} property instead of a
* [contentController]{@link Repetition#contentController}.
*
* Ensures that the document contains iterations in the same order as provided
* by the content controller.
*
* The repetition strives to avoid moving iterations on, off, or around on the
* document, prefering to inject or retract iterations between ones that remain
* in their respective order, or even just rebind existing iterations to
* alternate content instead of injecting and retracting in the same position.
* some content.
*
* @extends Component
*/
var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition.prototype # */{
// For the creator:
// ----
/**
* Imperatively initializes a repetition with content. You can alternately
* bind the `content` property of a repetition without initializing. You
* should not use the `contentController` property of the repetition if you
* are initialized with the `content` property.
* @private
*/
initWithContent: {
value: function (content) {
this.object = content;
return this;
}
},
/**
* Imperatively initializes a repetition with a content controller, like a
* `RangeController`. You can alternately bind the `contentController`
* property of a repetition without initializing. You should not use the
* `content` property of a repetition if you are using its
* `contentController`.
* @private
*/
initWithContentController: {
value: function (contentController) {
this.contentController = contentController;
return this;
}
},
/**
* A getter and setter for the content of a repetition. If you set the
* content property of a repetition, it produces a range content controller
* for you. If you get the content property, it will reach into the
* content controller to give you its content.
*
* The content represents the entire backing collection. The content
* controller may filter, sort, or otherwise manipulate the visible region
* of the content. The {@link Iteration#index} of each iteration
* corresponds to the position within the visible region of the controller.
* @type {Array.<Object>}
*/
content: {
get: function () {
if (this.contentController) {
return this.contentController.content;
}
return null;
},
set: function (value) {
this.contentController.content = value;
}
},
/**
* A range controller or instance with the same interface (
* [iterations]{@link Repetition#iterations} and
* [selection]{@link Repetition#selection} properties, where each
* <iteration has `object` and `selected` properties). The controller is
* responsible for managing which contents are visible, selected, and the
* order of their appearance.
* @type {RangeController}
*/
_contentController: {
value: null
},
contentController: {
set: function (contentController) {
if (this._contentController !== contentController) {
this._contentController = contentController;
}
},
get: function () {
if (!this._contentController) {
this._contentController = new RangeController();
}
return this._contentController;
}
},
/**
* When selection is enabled, each element in an iteration responds to
* touch and click events such that the iteration is highlighted (with the
* "active" CSS class) when the user touches or clicks it, and toggles
* whether the corresponding content is selected.
*
* Selection may be enabled and disabled at any time. The repetition
* watches changes to this property.
*
* All repetitions support selection, whether it is used or not. This
* property merely dictates whether the repetition handles gestures for
* selection.
* @type {boolean}
*/
isSelectionEnabled: {value: null},
allowsMultipleSelection: {
set: function (allowsMultipleSelection) {
allowsMultipleSelection = !!allowsMultipleSelection;
if (this.contentController.allowsMultipleSelection !== allowsMultipleSelection) {
this.contentController.allowsMultipleSelection = allowsMultipleSelection;
}
if (allowsMultipleSelection && !this.isSelectionEnabled) {
this.isSelectionEnabled = true;
}
},
get: function () {
return this.contentController !== void 0 && this.contentController !== null ?
this.contentController.allowsMultipleSelection : false;
}
},
/**
* The repetition maintains an array of every visible, selected iteration,
* in the order of its appearance. The user should not modify the selected
* iterations array.
* @type {Array.<Iteration>}
*/
selectedIterations: {value: null},
/**
* The repetition maintains an array of the indexes of every selected
* iteration. The user should not modify the array.
* @type {Array.<number>}
*/
selectedIndexes: {value: null},
/**
* The user may determine which iterations are active by setting or
* manipulating the content of the `activeIterations` array. At present,
* the repetition does not guarantee any particular order of appearance of
* the contained iterations.
* @type {Array.<Iteration>}
*/
activeIterations: {value: null},
/**
* The repetition coordinates this array of repetition iterations. Each
* iteration tracks its corresponding content, whether it is selected,
* whether it is active, and what CSS classes are applied on each of its
* direct child nodes. This array appears in the order that the iterations
* will be drawn. There is one repetition iteration for each controller
* iteration. The repetition iterations have more responsibilities than
* the corresponding controller, but some of the properties are bound by
* the same names, like `object` and `selected`.
* @type {Array.<Iteration>}
*/
iterations: {value: null},
// For the template:
// ----
/**
* Informs the super-type, `Component`, that there is no `repetition.html`.
* @private
*/
hasTemplate: {value: false},
/**
* A copy of `innerTemplate`, provided by the `Component` layer, that
* produces the HTML and components for each iteration. If this property
* is `null`, it signifies that the template is in transition, either
* during initialization or due to resetting `innerTemplate`. In either
* case, it is a reliable indicator that the repetition is responding to
* controller iteration range changes, since that requires a functioning
* template.
* @private
*/
_iterationTemplate: {value: null},
/**
* Informs Template that it is not safe to reference the initial DOM
* contents of the repetition.
* @see Component.clonesChildComponents
* @private
*/
clonesChildComponents: {value: true},
__pressComposer: {value: null},
_pressComposer: {
get: function () {
if (!this.__pressComposer) {
this.__pressComposer = new PressComposer();
this.__pressComposer.lazyLoad = true;
this.addComposerForElement(this.__pressComposer, this.element);
}
return this.__pressComposer;
}
},
// Implementation:
// ----
_cancelSelectionRangeChangeListener: {
value: null
},
_selection: {
value: null
},
selection: {
get: function () {
return this._selection;
},
set: function (value) {
if (this.contentController) {
if (this.contentController.selection !== value) {
this.contentController.selection = value;
}
if (this._selection !== this.contentController.selection) {
this._selection = this.contentController.selection;
}
if (this._cancelSelectionRangeChangeListener) {
this._cancelSelectionRangeChangeListener();
}
if (value) {
this._cancelSelectionRangeChangeListener = (
this.contentController.selection.addRangeChangeListener(this, "selection")
);
this.handleSelectionRangeChange(value, []);
} else {
this._cancelSelectionRangeChangeListener = null;
}
} else {
this._selection = value;
}
}
},
handleSelectionRangeChange: {
value: function (add, remove) {
if (this.iterations) {
var iterationsMap,
length = this.iterations.length,
objectIterations,
object,
iteration,
i, j;
if ((add.length <= 1) && (remove.length <= 1)) {
if (remove.length) {
object = remove[0];
for (i = 0; i < length; i++) {
if (this.iterations[i].object === object) {
this.iterations[i].selected = false;
}
}
}
if (add.length) {
object = add[0];
for (i = 0; i < length; i++) {
if (this.iterations[i].object === object) {
this.iterations[i].selected = true;
}
}
}
} else {
iterationsMap = new Map();
for (i = 0; i < length; i++) {
iteration = this.iterations[i];
object = iteration.object;
if (!(objectIterations = iterationsMap.get(object))) {
objectIterations = [];
iterationsMap.set(object, objectIterations);
}
objectIterations.push(iteration);
}
for (i = 0; i < remove.length; i++) {
if ((objectIterations = iterationsMap.get(remove[i]))) {
for (j = 0; j < objectIterations.length; j++) {
objectIterations[j].selected = false;
}
}
}
for (i = 0; i < add.length; i++) {
if ((objectIterations = iterationsMap.get(add[i]))) {
for (j = 0; j < objectIterations.length; j++) {
objectIterations[j].selected = true;
}
}
}
}
}
}
},
_visibleIndexes: {
value: null
},
visibleIndexes: {
get: function () {
return this._visibleIndexes;
},
set: function (value) {
if (this._visibleIndexes !== value) {
if (this._visibleIndexes && this._visibleIndexes.removeRangeChangeListener) {
this._visibleIndexes.removeRangeChangeListener(this, "visibleIndexes");
}
this._visibleIndexes = value;
if (this._visibleIndexes && this._visibleIndexes.addRangeChangeListener) {
this._visibleIndexes.addRangeChangeListener(this, "visibleIndexes");
}
this._updateOrganizedContent();
}
}
},
handleVisibleIndexesRangeChange: {
value: function (plus, minus, index) {
var plusContent,
i;
if (this.__controllerOrganizedContent) {
plusContent = [];
for (i = 0; i < plus.length; i++) {
plusContent.push(this.__controllerOrganizedContent[plus[i]]);
}
this.organizedContent.swap(index, minus.length, plusContent);
if (this._isListeningToOrganizedContentChanges) {
this.handleOrganizedContentRangeChange(plusContent.length, minus.length, index);
this.needsDraw = true;
}
}
}
},
__controllerOrganizedContent: {
value: null
},
_controllerOrganizedContent: {
get: function () {
return this.__controllerOrganizedContent;
},
set: function (value) {
if (this.__controllerOrganizedContent !== value) {
if (this.__controllerOrganizedContent && this.__controllerOrganizedContent.removeRangeChangeListener) {
this.__controllerOrganizedContent.removeRangeChangeListener(this, "controllerOrganizedContent");
}
this.__controllerOrganizedContent = value;
if (this.__controllerOrganizedContent && this.__controllerOrganizedContent.addRangeChangeListener) {
this.__controllerOrganizedContent.addRangeChangeListener(this, "controllerOrganizedContent");
}
this._updateOrganizedContent();
}
}
},
handleControllerOrganizedContentRangeChange: {
value: function (plus, minus, index) {
if (!this._visibleIndexes) {
this.organizedContent.swap(index, minus.length, plus);
if (this._isListeningToOrganizedContentChanges) {
this.handleOrganizedContentRangeChange(plus.length, minus.length, index);
this.needsDraw = true;
}
} else {
this._updateOrganizedContent();
}
}
},
_updateOrganizedContent: {
value: function () {
var previousLength,
i;
if (this.__controllerOrganizedContent) {
if (this._visibleIndexes) {
if (this.organizedContent.length > this._visibleIndexes.length) {
this.organizedContent.length = this._visibleIndexes.length;
if (this._isListeningToOrganizedContentChanges) {
this.handleOrganizedContentRangeChange(0, this.organizedContent.length - this._visibleIndexes.length, this._visibleIndexes.length);
}
}
previousLength = this.organizedContent.length;
for (i = 0; i < this._visibleIndexes.length; i++) {
if (this.organizedContent[i] !== this.__controllerOrganizedContent[this._visibleIndexes[i]]) {
this.organizedContent[i] = this.__controllerOrganizedContent[this._visibleIndexes[i]];
if (this._isListeningToOrganizedContentChanges) {
this.handleOrganizedContentRangeChange(1, previousLength > i ? 1 : 0, i);
}
}
}
if (this._isListeningToOrganizedContentChanges) {
this.needsDraw = true;
}
} else {
previousLength = this.organizedContent.length;
this.organizedContent.swap(0, previousLength, this.__controllerOrganizedContent);
if (this._isListeningToOrganizedContentChanges) {
this.handleOrganizedContentRangeChange(this.__controllerOrganizedContent.length, previousLength, 0);
this.needsDraw = true;
}
}
} else {
previousLength = this.organizedContent.length;
this.organizedContent = [];
if (this._isListeningToOrganizedContentChanges) {
this.handleOrganizedContentRangeChange(0, previousLength, 0);
this.needsDraw = true;
}
}
}
},
/**
* @private
*/
constructor: {
value: function Repetition() {
// XXX Note: Any property added to initialize in constructor must
// also be accounted for in _teardownIterationTemplate to reset the
// repetition.
this.contentController = null;
this.organizedContent = [];
this.defineBinding("_controllerOrganizedContent", {
"<-": "contentController.organizedContent"
});
// Determines whether the repetition listens for mouse and touch
// events to select iterations, which involves "activating" the
// iteration when the user touches.
this.isSelectionEnabled = false;
this.defineBinding("selection", {
"<-": "contentController.selection"
});
this.defineBinding("selectedIterations", {
"<-": "iterations.filter{selected}"
});
this.defineBinding("selectedIndexes", {
"<-": "selectedIterations.map{index}"
});
this.defineBinding("allowsMultipleSelection", {
"<-": "contentController.allowsMultipleSelection"
});
// The iteration template:
// ---
// The template that gets repeated in the DOM
this._iterationTemplate = null;
// This triggers the setup of the iteration template
this.addPathChangeListener(
this._setupRequirements,
this,
"_handleSetupRequirementsChange"
);
// This triggers the teardown of an iteration template.
this.addPathChangeListener(
"innerTemplate",
this,
"_handleInnerTemplateChange"
);
// The state of the DOM:
// ---
// The "iterations" array tracks "_controllerIterations"
// synchronously. Each iteration corresponds to controlled content
// at its visible position. An iteration has an instance of the
// iteration template / inner template.
this.iterations = [];
// The "_drawnIterations" array gets synchronized with
// "iterations" by applying draw operations when "Repetition.draw"
// occurs.
this._drawnIterations = [];
// Iteration content can be reused. When an iteration is collected
// (and when it is initially created), it gets put in the
// _freeIterations list.
this._freeIterations = []; // push/pop LIFO
// We track the direct child nodes of every iteration so we can
// look up which iteration a mouse or touch event occurs on, for
// the purpose of selection tracking.
this._iterationForElement = new Map();
// This promise synchronizes the creation of new iterations.
this._iterationCreationPromise = Promise.resolve();
// Where we want to be after the next draw:
// ---
// The _boundaries array contains comment nodes that serve as the
// top and bottom boundary of each iteration. There will always be
// one more boundary than iteration.
this._boundaries = [];
// The plan for the next draw to synchronize _controllerIterations
// and iterations on the DOM:
// ---
this._dirtyClassListIterations = new Set();
// We can draw when we have created all requested iterations.
this._requestedIterations = 0;
this._createdIterations = 0;
this._canDrawInitialContent = false;
this._initialContentDrawn = false;
// Selection gestures
// ------------------
this.addOwnPropertyChangeListener("isSelectionEnabled", this);
// Used by selection tracking (last part of Repetition
// implementation) to track which selection pointer the repetition
// is monitoring
this._selectionPointer = null;
// This is a list of iterations that are active. It is maintained
// entirely by a bidirectional binding to each iteration's "active"
// property, which in turn manages the "active" class on each
// element in the iteration in the draw cycle. Iterations are
// activated by gestures when selection is enabled, and can also be
// managed manually for a cursor, as in an autocomplete drop-down.
// TODO Provide some assurance that the activeIterations will
// always appear in the same order as they appear in the iterations
// list.
this.activeIterations = [];
}
},
// Creating an iteration template:
// ----
/**
* This is an FRB expression that becomes true when all of the requirements
* for setting up an iteration template have been satisfied.
* - A component is not able to get its innerTemplate before being
* completely deserialized from the template and self means having
* access to its ownerDocumentPart. This will happen when the
* repetition is asked to load its component tree during template
* instantiation.
* - We shouldn't set up the iteration template if the repetition
* received new content, we'll wait until contentDidLoad is called.
* The problem is that the new components from the new DOM are already
* in the component tree but not in the DOM, and since self function
* removes the child components from the repetition we lose them
* forever.
* @private
*/
_setupRequirements: {
value: "[!_iterationTemplate.defined(),!_newDomContent.defined(),!_shouldClearDomContentOnNextDraw,_isComponentExpanded,_ownerDocumentPart.defined()].every{}"
},
/**
* This is the rising-edge trigger for setting up the iteration template.
* When the `_setupRequirements` expression becomes true, it is time to set
* up the iteration template based on the inner template.
* @private
*/
_handleSetupRequirementsChange: {
value: function (canSetUp) {
if (canSetUp) {
this._setupIterationTemplate();
}
}
},
/**
* This is the falling-edge trigger that tears down the iteration template.
* A new iteration template will be created if or when an inner template
* is provided and all the requirements are satisfied again.
* @private
*/
_handleInnerTemplateChange: {
value: function (innerTemplate) {
if (this._iterationTemplate) {
this._teardownIterationTemplate();
}
if (innerTemplate && this.getPath(this._setupRequirements)) {
this._setupIterationTemplate();
}
}
},
/**
* Prepares this component and all its children for garbage collection
* (permanently) or reuse.
*
* @param permanently whether to cancel bindings on this component
* and all of its descendants in the component tree.
* @private
*/
cleanupDeletedComponentTree: {
value: function (permanently) {
// Don't set innerTemplate directly because the listener system
// will get it and that will make the repetition to create it all
// over again if it happens to be null for some reason.
var previousIterationTemplate = this._innerTemplate;
this._innerTemplate = null;
if (previousIterationTemplate) {
this._teardownIterationTemplate();
}
if (permanently) {
this.cancelBindings();
}
}
},
/**
* Called by Component to build the component tree.
* @private
*/
expandComponent: {
value: function expandComponent() {
// Setting this property to true *causes* _setupIterationTemplate
// to be run through the handleSetupRequirementsChange listener,
// and as it runs synchronously, guarantees that the template will
// be expanded before the next line.
this._isComponentExpanded = true;
// TODO should this ever become false?
return Promise.resolve();
}
},
_buildIterationTemplate: {
value: function () {
var iterationTemplate;
var serialization;
var serializationObject;
var label;
// We need to clone the innerTemplate because this repetition
// might be used in different contexts and with different template
// arguments making it having diferent external objects in its
// instances.
iterationTemplate = this.innerTemplate.clone();
serialization = iterationTemplate.getSerialization();
serializationObject = serialization.getSerializationObject();
label = Montage.getInfoForObject(this).label;
this._iterationLabel = label + ":iteration";
serializationObject[this._iterationLabel] = {};
iterationTemplate.setObjects(serializationObject);
if (this.innerTemplate.hasParameters()) {
this._expandIterationTemplateParameters(iterationTemplate);
}
//jshint -W106
if (window._montage_le_flag) {
iterationTemplate.refresher = this;
this._leTagIterationTemplate(iterationTemplate);
}
//jshint +W106
return iterationTemplate;
}
},
_rebuildIterationTemplate: {
value: function () {
var iterationTemplate = this._iterationTemplate,
newIterationTemplate,
iterations = this.iterations;
this._purgeFreeIterations();
for (var i = 0, iteration; (iteration = iterations[i]); i++) {
iteration.isDirty = true;
}
this._innerTemplate = null;
newIterationTemplate = this._buildIterationTemplate();
iterationTemplate.replaceContentsWithTemplate(newIterationTemplate);
}
},
refreshTemplate: {
value: function () {
this._rebuildIterationTemplate();
}
},
_isListeningToOrganizedContentChanges: {
value: false
},
/**
* When `_setupRequirements` have all been met, this method produces an
* iteration template using the `innerTemplate` that has been given to this
* repetition. It also deletes any *initial* child components and starts
* watching for changes to the organized content. Watching for organized
* content changes would cause errors if it were not possible to
* instantiate iterations. In symmetry, `_teardownIterationTemplate`
* pauses watching the organized content.
* @private
*/
_setupIterationTemplate: {
value: function () {
this._iterationTemplate = this._buildIterationTemplate();
// Erase the initial child component trees. The initial document
// children will be purged on first draw. We use the innerTemplate
// as the iteration template and replicate it for each iteration
// instead of using the initial DOM and components.
var childComponents = this.childComponents;
var childComponent;
var index = childComponents.length - 1;
// pop() each component instead of shift() to avoid bubbling the
// indexes of each child component on every iteration.
while ((childComponent = childComponents[index--])) {
childComponent.detachFromParentComponent();
childComponent.needsDraw = false;
childComponent.cleanupDeletedComponentTree(true); // cancel bindings, permanent
}
// Begin tracking the controller organizedContent. We manually
// dispatch a range change to track all the iterations that have
// come and gone while we were not watching.
this.handleOrganizedContentRangeChange(this.organizedContent.length, 0, 0);
// Dispatches handleOrganizedContentRangeChange:
this._isListeningToOrganizedContentChanges = true;
this._canDrawInitialContent = true;
this.needsDraw = true;
}
},
_leTagIterationTemplate: {
value: function (template) {
var body = template.document.body;
if (body.children.length > 0) {
//jshint -W106
var ownerModuleId = this.ownerComponent._montage_metadata.moduleId;
var label = this._montage_metadata.label;
//jshint +W106
this._leTagStarArgument(ownerModuleId, label, body);
}
}
},
/**
* This method is used both in `cleanupDeletedComponentTree` and the
* internal `_handleInnerTemplateChange` functions, to retract all drawn
* iterations from the document, prepare all allocated iterations for
* garbage collection, and pause observation of the controller's
* iterations.
* @private
*/
_teardownIterationTemplate: {
value: function () {
// stop listenting to controlled content changes until the new
// iteration template is ready. (at which point we will manually
// dispatch handleOrganizedContentRangeChange with the entire
// content of the array when _setupIterationTemplate has finished)
this._isListeningToOrganizedContentChanges = false;
// simulate removal of all iterations from the controller to purge
// the iterations and _drawnIterations.
this.handleOrganizedContentRangeChange(0, this.organizedContent.length, 0);
// prepare all the free iterations and their child component trees
// for garbage collection
this._purgeFreeIterations();
// purge the existing iterations
this._iterationTemplate = null;
this._iterationForElement.clear();
this._requestedIterations = 0;
this._createdIterations = 0;
this._canDrawInitialContent = false;
this._selectionPointer = null;
this.activeIterations.clear();
this._dirtyClassListIterations.clear();
}
},
_purgeFreeIterations: {
value: function () {
for (var i = 0; i < this._freeIterations.length; i++) {
var iteration = this._freeIterations[i];
for (var j = 0; j < iteration._childComponents.length; j++) {
var childComponent = iteration._childComponents[j];
this.removeChildComponent(childComponent);
childComponent.cleanupDeletedComponentTree(true); // true cancels bindings
}
}
this._freeIterations.clear();
}
},
// TODO(@aadsm) doc
/**
* @private
*/
_expandIterationTemplateParameters: {
value: function (template) {
var owner = this,
argumentsTemplate,
collisionTable,
externalLabels,
objects,
instances,
expansionResult,
newLabel,
labels,
metadata;
// Crawl up the template chain while there are parameters to expand
// in the iteration template.
while (template.hasParameters()) {
owner = owner.ownerComponent;
argumentsTemplate = owner._ownerDocumentPart.template;
objects = owner._ownerDocumentPart.objects;
expansionResult = template.expandParameters(owner);
// Associate the new external objects with the objects in the
// instantiation of argumentsTemplate.
externalLabels = template.getSerialization()
.getExternalObjectLabels();
instances = template.getInstances();
labels = expansionResult.labels;
collisionTable = expansionResult.labelsCollisions;
for (var i = 0, label; (label = labels[i]); i++) {
if (collisionTable && label in collisionTable) {
newLabel = collisionTable[label];
} else {
newLabel = label;
}
// Setup external objects and configure the correct require,
// label and owner for the objects that came from the
// template arguments.
if (externalLabels.indexOf(newLabel) >= 0) {
instances[newLabel] = objects[label];
} else {
metadata = argumentsTemplate.getObjectMetadata(label);
if (!metadata.owner) {
metadata.owner = objects.owner;
}
template.setObjectMetadata(newLabel, metadata.require,
metadata.label, metadata.owner);
}
}
}
}
},
// Instantiating an iteration template:
// ----
_iterationLabel: {
value: null
},
/**
* We can only create one iteration at a time because it is an asynchronous
* operation and the "repetition.currentIteration" property may be bound
* during this process. If we were to attempt to instantiate multiple
* iterations asynchronously, currentIteration and contentAtCurrentIteration
* bindings would get interleaved. The "_iterationCreationPromise"
* synchronizes "createIteration", ensuring we only create one at a time,
* waiting for the previous to either succeed or fail before attempting
* another.
* @private
*/
_iterationCreationPromise: {value: null},
/**
* Creates a new iteration and sets up a new instance of the iteration
* template. Ensures that only one iteration is being instantiated at a
* time to guarantee that `currentIteration` can be reliably bound to the
* particular iteration.
* @private
*/
_createIteration: {
value: function () {
var self = this,
iteration = new this.Iteration().initWithRepetition(this);
this._iterationCreationPromise = this._iterationCreationPromise
.then(function () {
var _document = self.element.ownerDocument,
instances,
promise;
// We need to extend the instances of the template to add the
// iteration object that is specific to each iteration template
// instance.
instances = self._iterationTemplate.getInstances();
instances = Object.create(instances);
instances[self._iterationLabel] = iteration;
promise = self._iterationTemplate.instantiateWithInstances(instances, _document)
.then(function (part) {
part.parentDocumentPart = self._ownerDocumentPart;
iteration._templateDocumentPart = part;
part.loadComponentTree().then(function () {
if (logger.isDebug) {
logger.debug("Iteration:%s component tree loaded.", Object.hash(iteration));
}
iteration._fragment = part.fragment;
// It is significant that _childComponents are assigned
// *after* the component tree has finished loading
// because this signals to the iteration that it should
// synchronize the child components with the repetition
// based on whether the iteration should be on the DOM
// hereafter.
iteration._childComponents = part.childComponents;
self.constructIteration(iteration);
});
return iteration;
});
return promise.then(null, function () {
// but regardless of whether this iteration failed, allow
// another iteration to be created
});
});
this._requestedIterations++;
return iteration;
}
},
/**
* @private
*/
// This utility method for the completion of _createIteration.
constructIteration: {
value: function (iteration) {
this._createdIterations++;
if (this._createdIterations >= this._requestedIterations) {
this.needsDraw = true;
// TODO: When we change the canDraw() function of a component
// we need to _canDraw = true whenever we request a draw.
// This is because if the component gets into a state where it
// is part of the draw cycle but not able to draw (canDraw()
// === false) its needsDraw property is not set to false and
// further needsDraw = true will result in a noop, the only way
// to make the component draw again is by informing the root
// component directly that it can draw now, and this is done by
// _canDraw = true. Another option is to make its parent draw,
// but we probably don't want that.
this._canDraw = true;
}
}
},
// Reacting to changes in the controlled visible content:
// ----
/**
* The content controller produces an array of iterations. The controller
* may come and go, but each instance of a repetition has its own array to
* track the corresponding content controller's content, which gets emptied
* and refilled by a range content binding when the controller changes.
* This is to simplify management of the repetition's controller iterations
* range change listener.
*
* The controller iterations themselves instruct the repetition to display
* an iteration at the corresponding position, and provide a convenient
* interface for getting and setting whether the corresponding content is
* selected.
* @private
*/
_controllerIterations: {value: null},
/**
* The drawn iterations get synchronized with the `iterations` array each
* time the repetition draws. The `draw` method simply walks down the
* iterations and drawn iterations arrays, redacting drawn iterations if
* they are not at the correct position and injecting the proper iteration
* from the model in its place.
* @private
*/
_drawnIterations: {value: null},
/**
* @private
*/
_freeIterations: {value: null},
/**
* Reacts to changes in the controller's organized content by altering the
* modeled iterations. This may require additional iterations to be
* instantiated. The repetition may redraw when all of the instantiated
* iterations have finished loading.
*
* This method is dispatched in response to changes to the organized
* content but only while the repetition is prepared to instantiate
* repetitions. Any time the repetition needs to change its inner
* template, or when it is setting up its initial inner template, the
* repetition silences the organizedContent range change listener and
* manually calls this method as if organizedContent were cleared out, to
* cause all of the iterations to be collected and removed from the
* document. When the iteration template is ready again, it manually
* dispatches this method again as if the organizedContent had been
* repopulated, then resumes listening for changes.
*
* Bindings react instantly to the change in the iteration model. The draw
* method synchronizes `index` and `_drawnIndex` on each iteration as it
* rearranges `_drawnIterations` to match the order and content of the
* `iterations` array.
*
* @private
*/
handleOrganizedContentRangeChange: {
value: function (plusLength, minusLength, index) {
var start = index,
freedIterations,
freedIteration,
newIterations, // isDebugOnly
iterations = this.iterations,
reusableIterationsCount = Math.min(plusLength, minusLength),
removeIterationsCount = minusLength - reusableIterationsCount,
addIterationsCount = plusLength - reusableIterationsCount,
organizedContent = this.organizedContent,
i, j;
if (logger.isDebug) {
logger.debug("Repetition:%s content changed +%s@%s %O -%s %O ", Object.hash(this), plusLength?plusLength:0, index, minusLength?minusLength:0);
logger.debug("Repetition:%s +%s -%s iterations", Object.hash(this), addIterationsCount, removeIterationsCount);
}
if (this._iterationTemplate.isDirty) {
this._iterationTemplate.refresh();
}
// This is an optimization for a common case with the Flow that
// avoids shifting around with the iterations and free iterations
// array. If the number of added and removed content values are
// the same, which is what happens if a value is set at an index,
// it is unnecessary to splice the corresponding index in and out,
// and unnecessary to even check whether more iterations need to be
// allocated.
for (i = 0; i < reusableIterationsCount; i++, index++) {
iterations[index].object = organizedContent[index];
}
if (removeIterationsCount > 0) {
// Subtract iterations
freedIterations = iterations.splice(index, removeIterationsCount);
// Notify these iterations that they have been recycled,
// particularly so they know to disable animations with the
// "no-transition" CSS class.
// Add them back to the free list so they can be reused
for (i = 0; i < removeIterationsCount; i++) {
freedIteration = freedIterations[i];
freedIteration.recycle();
if (!freedIteration.isDirty) {
this._freeIterations.push(freedIteration);
}
}
}
if (logger.isDebug) {
newIterations = [];
}
if (addIterationsCount > 0) {
// Create more iterations if we will need them
while (this._freeIterations.length < addIterationsCount) {
this._freeIterations.push(this._createIteration());
if (logger.isDebug) {
newIterations.push(this._freeIterations[this._freeIterations.length-1]);
}
}
// Add iterations
var plusIterations = new Array(addIterationsCount);
for (i = reusableIterationsCount, j = 0; i < plusLength; i++, j++) {
var iteration = this._freeIterations.pop();
if (logger.isDebug) {
if(!newIterations.has(iteration)) {
logger.debug("Repetition:%s reusing %s", Object.hash(this), Object.hash(iteration));
}
}
var content = organizedContent[start + i];
iteration.object = content;
plusIterations[j] = iteration;
}
iterations.swap(index, 0, plusIterations);
}
if (removeIterationsCount > 0 || addIterationsCount > 0) {
// Update indexes for all subsequent iterations
this._updateIndexes(index);
}
}
},
/**
* Used by handleOrganizedContentRangeChange to update the controller index
* of every iteration following a change.
* @private
*/
_updateIndexes: {
value: function (index) {
var iterations = this.iterations;
for (; index < iterations.length; index++) {
iterations[index].index = index;
}
}
},
_addDirtyClassListIteration: {
value: function (iteration) {
iteration.forEachElement(function (element) {
var component;
if (element && (component = element.component)) {
// If the element has a component then use the component's
// classList and let it handle drawing...
component.classList[iteration.active ? "add" : "remove"]("active");
component.classList[iteration.selected ? "add" : "remove"]("selected");
component.classList.remove("no-transition");
} else {
// ...otherwise we will handle the drawing of the classes
// on plain elements ourselves
this._dirtyClassListIterations.add(iteration);
}
}, this);
}
},
/**
* @private
*/
canDraw: {
value: function () {
// block for the usual component-related issues
var canDraw = this.canDrawGate.value;
// block until we have created enough (iterations to draw
canDraw = canDraw && this._requestedIterations <= this._createdIterations;
// block until we can draw initial content if we have not already
canDraw = canDraw && (this._initialContentDrawn || this._canDrawInitialContent);
// TODO: we're going to comment this out for now at least because
// the repetition can get into a dead lock in the case of a nested
// repetition (a repetition that has another repetition as direct
// child component). It's possible to get into a state where the
// inner repetition will never be able to draw unless the outer
// repetition draws first. Hopefully the DrawManager will be able
// to solve this. - @aadsm
// block until all child components can draw
//if (canDraw) {
// for (var i = 0; i < this.childComponents.length; i++) {
// var childComponent = this.childComponents[i];
// if (!childComponent.canDraw()) {
// canDraw = false;
// }
// }
//}
return canDraw;
}
},
/**
* An array of comment nodes that mark the boundaries between iterations on
* the DOM. When an iteration is retracted, the top boundary gets
* retracted with it so the iteration at index N will always have boundary
* N above it and N + 1 below it. There must always be one more boundary
* than there are iterations, representing the bottom boundary of the last
* iteration. That boundary gets added in first draw.
* @private
*/
_boundaries: {value: null},
/**
* A Set of iterations that have changed their CSS classes that are managed
* by the repetition, "active", "selected", and "no-transition".
* @private
*/
_dirtyClassListIterations: {value: null},
/**
* The cumulative number of iterations that _createIteration has started
* making.
* @private
*/
_requestedIterations: {value: null},
/**
* The cumulative number of iterations that _createIteration has finished
* making.
* @private
*/
_createdIterations: {value: null},
/**
* In the first draw, the repetition gets rid of its innerHTML, which was
* captured by the innerTemplate, and replaces it with the bottom boundary
* marker comment. This cannot be done until after the iteration template
* is ready.
*
* This cycle may occur again if the innerTemplate is replaced.
* @private
*/
_canDrawInitialContent: {value: null},
/**
* Indicates that the first draw has come and gone and the repetition is
* ready for business.
* @private
*/
_initialContentDrawn: {value: null},
/**
* @private
*/
draw: {
value: function () {
var index;
if (!this._initialContentDrawn) {
this._drawInitialContent();
this._initialContentDrawn = true;
}
// Synchronize iterations and _drawnIterations
// Retract iterations that should no longer be visible
for (index = this._drawnIterations.length - 1; index >= 0; index--) {
if (this._drawnIterations[index].index === null) {
this._drawnIterations[index].retractFromDocument();
}
}
// Inject iterations if they are not already in the right location
for (index = 0; index < this.iterations.length; index++) {
var iteration = this.iterations[index];
if (iteration._drawnIndex !== iteration.index && iteration.isComponentTreeLoaded()) {
iteration.injectIntoDocument(index);
}
}
// Update class lists
var iterations = this._dirtyClassListIterations.toArray();
// Note that the iterations list must be cleared first because we
// remove the "no-transition" class during the update if we find
// it, which in turn schedules another draw and adds the iteration
// back to the schedule.
this._dirtyClassListIterations.clear();
iterations.forEach(function (iteration) {
if(iteration.isComponentTreeLoaded()) {
iteration.forEachElement(function (element) {
// Only update classes that don't have a component, they
// are taken care of in _addDirtyClassListIteration
if (element.component) {
return;
}
element.classList[iteration.active ? "add" : "remove"]("active");
element.classList[iteration.selected ? "add" : "remove"]("selected");
// While we're at it, if the "no-transition" class has been
// added to this iteration, we will need to remove it in
// the next draw to allow the iteration to animate.
element.classList.remove("no-transition");
}, this);
}
}, this);
}
},
/**
* @private
*/
_drawInitialContent: {
value: function () {
var element = this.element;
var childNodesCount = element.childNodes.length;
for (var i = 0; i < childNodesCount; i++) {
element.removeChild(element.firstChild);
}
var bottomBoundary = element.ownerDocument.createTextNode("");
element.appendChild(bottomBoundary);
this._boundaries.push(bottomBoundary);
}
},
/**
* @private
*/
// Used by the insertion and retraction operations to update the drawn
// indexes of every iteration following a change.
_updateDrawnIndexes: {
value: function (drawnIndex) {
var drawnIterations = this._drawnIterations;
for (; drawnIndex < drawnIterations.length; drawnIndex++) {
drawnIterations[drawnIndex]._drawnIndex = drawnIndex;
}
}
},
// Selection Tracking
// ------------------
/**
* If `isSelectionEnabled`, the repetition captures the pointer, preventing
* it from passing to parent components, for example for the purpose of
* scrolling.
* @private
*/
_selectionPointer: {value: null},
/**
* Original vertical coordinate of a touchstart/mousedown
*
* @type {number}
* @private
*/
_startX: { value: 0 },
/**
* Original horizontal coordinate of a touchstart/mousedown
*
* @type {number}
* @private
*/
_startY: { value: 0 },
/**
* Pointer to the current active Iteration
*
* @type {object}
* @private
*/
_currentActiveIteration: { value: null },
/**
* @private
*/
// Called by constructor to monitor changes to isSelectionEnabled and arrange
// the appropriate event listeners.
handleIsSelectionEnabledChange: {
value: function (selectionTracking) {
if (selectionTracking) {
this._enableSelectionTracking();
} else {
this._disableSelectionTracking();
}
}
},
/**
* @private
*/
// Called by handleIsSelectionEnabledChange in response to
// isSelectionEnabled becoming true.
_enableSelectionTracking: {
value: function () {
this._pressComposer.addEventListener("pressStart", this, false);
}
},
/**
* @private
*/
// Called by handleIsSelectionEnabledChange in response to
// isSelectionEnabled becoming false.
_disableSelectionTracking: {
value: function () {
this._pressComposer.removeEventListener("pressStart", this, false);
}
},
handlePressStart: {
value: function (pressEvent) {
var iteration = this._findIterationContainingElement(pressEvent.targetElement);
if (iteration) {
this._startX = pressEvent.clientX;
this._startY = pressEvent.clientY;
this.__pressComposer.addEventListener("press", this, false);
this.__pressComposer.addEventListener("pressCancel", this, false);
iteration.shouldBecomeActive = true;
this._currentActiveIteration = iteration;
}
}
},
/**
* @private
*/
_ignoreSelection: {
value: function () {
if (this._currentActiveIteration) {
this._currentActiveIteration.shouldBecomeActive = false;
this._currentActiveIteration.active = false;
this._currentActiveIteration = null;
}
this.activeIterations.clear();
this._startX = 0;
this._startY = 0;
this.__pressComposer.removeEventListener("press", this, false);
this.__pressComposer.removeEventListener("pressCancel", this, false);
}
},
/**
* @private
*/
handlePressCancel: {
value: function () {
this._ignoreSelection();
}
},
handlePress: {
value: function (event) {
var iteration = this._findIterationContainingElement(event.targetElement);
// And select it, if there is one
if (iteration && this._currentActiveIteration === iteration) {
iteration.active = false;
if (!iteration.selected) {
iteration.selected = true;
} else if (this.allowsMultipleSelection) {
iteration.selected = false;
}
}
this._ignoreSelection();
}
},
// ---
/**
* Finds the iteration that contains an element within the repetition.
* This requires the repetition to maintain an index of all of the
* <em>shallow</em> child elements of an iteration, _iterationForElement.
* It does so in the Iteration.injectIntoDocument, but is only
* approximately accurate since technically the child components of an
* iteration may add and remove siblings after injection. For the purpose
* of selection, we caution the user to wrap any dynamic elements in a
* static wrapper.
* @private
*/
_findIterationContainingElement: {
value: function (element) {
// Walk the document upward until we find the repetition and
// a direct child of the repetition element. The direct
// child must be tracked by the repetition.
var child;
while (element) {
if (element === this.element) {
return this._iterationForElement.get(child);
}
child = element;
element = element.parentNode;
}
}
},
// Polymorphic helper types
// ------------------------
/**
* The Iteration type for this repetition. The repetition calls `new
* this.Iteration()` to make new instances of iterations, so a child class
* of `Repetition` may provide an alternate implementation of `Iteration`.
* @private
*/
Iteration: { value: Iteration, serializable: false }
});