todomvc
Version:
> Helping you select an MV\* framework
1,235 lines (1,103 loc) • 64.7 kB
JavaScript
montageDefine("6364dae","ui/repetition.reel/repetition",{dependencies:["montage","ui/component","core/template","core/range-controller","core/promise","collections/map","collections/set","frb/observers"],factory:function(require,exports,module){var Montage = require("montage").Montage;
var Component = require("ui/component").Component;
var Template = require("core/template").Template;
var RangeController = require("core/range-controller").RangeController;
var Promise = require("core/promise").Promise;
var Map = require("collections/map");
var Set = require("collections/set");
var Observers = require("frb/observers");
var observeProperty = Observers.observeProperty;
var observeKey = Observers.observeKey;
/**
* A reusable view-model for each iteration of a repetition. Each iteration
* corresponds to a value from the 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.
*/
var Iteration = exports.Iteration = Montage.specialize({
/**
* The parent repetition component.
*/
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.
*/
controller: {value: null},
/**
* The corresponding content for this iteration.
*/
object: {value: null},
/**
* Whether the content for this iteration is selected. This property is
* bound bidirectionally to whether every element on the document for the
* corresponding drawn iteration has the `selected` CSS class (synchronized
* on draw), and whether the `object` is in the
* `contentController.selection` collection.
*/
selected: {value: null},
/**
* 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.
*/
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.
*/
active: {value: null},
/**
* Whether this iteration appears first in the visible order of iterations.
*/
isFirst: {value: null},
/**
* Whether this iteration appears last in the visible order of iterations.
*/
isLast: {value: null},
/**
* Whether this iteration appears on the 0th position within the iteration,
* or every other position thereafter.
*/
isEven: {value: null},
/**
* Whether this iteration appears on the 1st position within the iteration,
* or every other position thereafter.
*/
isOdd: {value: null},
/**
* 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},
/**
* Creates the initial values of all instance state.
* @private
*/
constructor: {
value: function Iteration() {
this.super();
this.repetition = null;
this.controller = null;
this.content = null;
this.defineBinding("object", {"<->": "content"}); // TODO remove this migration shim
// The iteration watches whether it is selected. If the iteration
// is drawn, it enqueue's selection change draw operations and
// notifies the repetition it needs to be redrawn.
// Dispatches handlePropertyChange with the "selected" key:
this.defineBinding("selected", {
"<->": "repetition.contentController._selection.has(content)"
});
// 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;
// The corresponding "content" is tracked in
// repetition._contentForIteration instead of on the iteration
// itself. The bindings in the iteration template react to changes
// in that map.
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;
// Describes whether a user gesture is touching this iteration.
this.active = false;
// 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.
// Dispatches handlePropertyChange with the "active" key:
this.defineBinding("active", {"<->": "repetition.activeIterations.has(())"});
this.defineBinding("isFirst", {"<-": "index == 0"});
this.defineBinding("isLast", {"<-": "index == repetition.iterations.length - 1"});
this.defineBinding("isEven", {"<-": "index % 2 == 0"});
this.defineBinding("isOdd", {"<-": "index % 2 != 0"});
this._noTransition = false;
// dispatch handlePropertyChange:
this.addOwnPropertyChangeListener("active", this);
this.addOwnPropertyChangeListener("selected", this);
this.addOwnPropertyChangeListener("_noTransition", this);
this.addPathChangeListener(
"index.defined() && _childComponents.defined()",
this,
"handleComponentModelChange"
);
this.cachedFirstElement = null;
}
},
/**
* Associates the iteration instance with a repetition.
*/
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.
*/
recycle: {
value: function () {
this.index = null;
this.content = 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.
*/
injectIntoDocument: {
value: function (index) {
if (this._drawnIndex !== null) {
this.retractFromDocument();
}
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);
// 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
for (var i = 0; i < this._childComponents.length; i++) {
var childComponent = this._childComponents[i];
childComponent.addEventListener("firstDraw", firstDraw, false);
childComponent.needsDraw = true;
}
repetition._drawnIterations.splice(index, 0, this);
repetition._updateDrawnIndexes(index);
repetition._addDirtyClassListIteration(this);
}
},
/**
* Retracts an iteration from the document, scooping its child nodes into
* its DOMFragment.
*/
retractFromDocument: {
value: function () {
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) {
if (onComponentModel) {
this._childComponents.forEach(
this.repetition.addChildComponent,
this.repetition
);
// the second condition protects against removing before adding in
// the initial state.
} else if (this._childComponents) {
this._childComponents.forEach(
this.repetition.removeChildComponent,
this.repetition
);
}
}
},
/**
* 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
*/
handlePropertyChange: {
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)
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`.
*/
firstElement: {
get: function () {
var repetition = this.repetition;
var index = this._drawnIndex;
if (index == null)
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;
}
}
}
},
/**
* The most recent result of the `firstElement` accessor, useful for speed
* if you know that the internal structure of the iteration is static.
*/
cachedFirstElement: {
value: null
}
});
// Here it is, what we have all been waiting for, the prototype of the hour.
// Give it up for the Repetition...
/**
* 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 `RangeController` for the content if you provide a `content`
* property instead of a `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.
* @class Repetition
* @extends Component
*/
var Repetition = exports.Repetition = Component.specialize(/** @lends Repetition# */{
// 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.
*/
initWithContent: {
value: function (content) {
this.content = 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`.
*/
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 `index` of each iteration corresponds to the
* position within the visible region of the controller.
*/
content: {
get: function () {
return this.getPath("contentController.content");
},
set: function (content) {
// TODO if we provide an implicit content controller, it should be
// excluded from a serialization of the repetition.
this.contentController = new RangeController().initWithContent(content);
}
},
/**
* A range controller or instance with the same interface (`iterations` and
* `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.
*/
contentController: {value: null},
/**
* 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 in the life cycle of
* the repetition. 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.
*/
isSelectionEnabled: {value: null},
/**
* A collection of the selected content. It may be any ranged collection
* like Array or SortedSet. The user may get, set, or modify the selection
* directly. The selection property is bidirectionally bound to the
* selection of the content controller. Every repetition has a content
* controller, and will use a RangeController if not given one.
*/
selection: {value: null},
/**
* 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.
*/
selectedIterations: {value: null},
/**
* The repetition maintains an array of the indexes of every selected
* iteration. The user should not modify the array.
*/
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 appearnce of
* the contained iterations.
*/
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`.
*/
iterations: {value: null},
/**
* The user may bind to the `currentIteration` when the repetition
* instantiates a new iteration. The template guarantees that child
* components can safely bind to the containing repetition.
*
* At present, you cannot bind to a grandparent repetition's
* `currentIteration`, so it becomes the responsibility of the parent
* repetition to bind its parent repetition's `currentIteration` to a
* property of itself so its children can access their grandparent.
*/
currentIteration: {value: null},
/**
* The user may bind the the `currentIteration.object` with this shorthand.
*/
contentAtCurrentIteration: {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},
// Implementation:
// ----
/**
* @private
*/
constructor: {
value: function Repetition() {
this.super();
// 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("organizedContent.rangeContent()", {
"<-": "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}"
});
// 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
// Whenever an iteration template is instantiated, it may have
// bindings to the repetition's "contentAtCurrentIteration". The
// repetition delegates "contentAtCurrentIteration" to a mapping
// from iterations to content, which it can dynamically update as
// the iterations are reused, thereby updating the bindings.
this._contentForIteration = Map();
// 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 = Map();
// This variable is updated in the context of deserializing the
// iteration template so bindings to "contentAtCurrentIteration" are
// attached to the proper "iteration". The "_contentForIteration"
// provides the level of indirection that allows iterations to be
// paired with different content during their lifetime, but the
// template and components for each iteration will always be tied
// to the same Iteration instance.
this.currentIteration = null;
// A memo key used by Template.createWithComponent to uniquely
// identify this repetition (and equivalent instances if this is
// nested in another repetition) so that it can memoize the
// template instance:
this._templateId = null;
// 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 = 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.
*/
cleanupDeletedComponentTree: {
value: function (permanently) {
// This also causes _iterationTemplate to be torn down, through
// handleInnerTemplateChange.
this.innerTemplate = null;
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();
}
},
/**
* 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 () {
var self = this;
if (self.innerTemplate.hasParameters()) {
self._iterationTemplate = self.innerTemplate.clone();
self._expandIterationTemplateParameters();
} else {
self._iterationTemplate = self.innerTemplate;
}
// 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 = self.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.
self.handleOrganizedContentRangeChange(self.organizedContent, [], 0);
// Dispatches handleOrganizedContentRangeChange:
self.organizedContent.addRangeChangeListener(self, "organizedContent");
self._canDrawInitialContent = true;
self.needsDraw = true;
}
},
/**
* 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.organizedContent.removeRangeChangeListener(this, "organizedContent");
// simulate removal of all iterations from the controller to purge
// the iterations and _drawnIterations.
this.handleOrganizedContentRangeChange([], this.organizedContent, 0);
// prepare all the free iterations and their child component trees
// for garbage collection
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
}
}
// purge the existing iterations
this._iterationTemplate = null;
this._freeIterations.clear();
this._contentForIteration.clear();
this._iterationForElement.clear();
this.currentIteration = null;
this._templateId = null;
this._requestedIterations = 0;
this._createdIterations = 0;
this._canDrawInitialContent = false;
this._selectionPointer = null;
this.activeIterations.clear();
this._dirtyClassListIterations.clear();
}
},
// TODO(@aadsm) doc
/**
* @private
*/
_expandIterationTemplateParameters: {
value: function() {
var template = this._iterationTemplate,
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(argumentsTemplate, 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:
// ----
/**
* 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;
self.currentIteration = iteration;
var promise = self._iterationTemplate.instantiate(_document)
.then(function (part) {
part.loadComponentTree().then(function() {
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);
}).done();
self.currentIteration = null;
})
promise.done(); // radiate an error if necessary
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;
}
}
},
/**
* This ties `contentAtCurrentIteration` to an iteration.
* `currentIteration` is only current in the stack of instantiating a
* template, so this method is a hook that the redirects
* `contentAtCurrentIteration` property change listeners to a map change
* listener on the `_contentForIteration` map instead. The binding then
* reacts to changes to the map as iterations are reused with different
* content at different positions in the DOM.
* @private
*/
observeProperty: {
value: function (key, emit, scope) {
if (key === "contentAtCurrentIteration" || key === "objectAtCurrentIteration") {
// delegate to the mapping from iterations to content for the
// current iteration
return observeKey(
this._contentForIteration,
this.currentIteration,
emit,
scope
);
} else if (key === "currentIteration") {
// Shortcut since this property is sticky -- won't change in
// the course of instantiating an iteration and should not
// dispatch a change notification when we instantiate the next.
return emit(this.currentIteration);
} else {
// fall back to normal property observation
return observeProperty(this, key, emit, scope);
}
}
},
/**
* This makes bindings to `currentIteration` stick regardless of how the
* repetition manipulates the property, and prevents a getter/setter pair
* from being attached to the property. `makePropertyObservable` is called
* by in the `listen/property-changes` module in the Collections package.
* @private
*/
makePropertyObservable: {
value: function (key) {
if (key !== "currentIteration") {
return Montage.makePropertyObservable.call(this, key);
}
}
},
// 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},
/**
* @private
*/
_contentForIteration: {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 (plus, minus, index) {
// Subtract iterations
var freedIterations = this.iterations.splice(index, minus.length);
freedIterations.forEach(function (iteration) {
// Notify these iterations that they have been recycled,
// particularly so they know to disable animations with the
// "no-transition" CSS class.
iteration.recycle();
});
// Add them back to the free list so they can be reused
this._freeIterations.addEach(freedIterations);
// Create more iterations if we will need them
while (this._freeIterations.length < plus.length) {
this._freeIterations.push(this._createIteration());
}
// Add iterations
this.iterations.swap(index, 0, plus.map(function (content, offset) {
var iteration = this._freeIterations.pop();
iteration.content = content;
// This updates the "repetition.contentAtCurrentIteration"
// bindings.
this._contentForIteration.set(iteration, content);
return iteration;
}, this));
// Update indexes for all subsequent iterations
this._updateIndexes(index);
this.needsDraw = true;
}
},
/**
* 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)) {