UNPKG

todomvc

Version:

> Helping you select an MV\* framework

1,235 lines (1,103 loc) 64.7 kB
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&eacute;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)) {