/*global Element, console */
/**
* @module montage/ui/component
* @requires montage
* @requires montage/core/target
* @requires montage/core/template
* @requires montage/core/document-resources
* @requires montage/core/gate
* @requires montage/core/promise
* @requires montage/core/logger
* @requires montage/core/event/event-manager
* @requires montage/core/serialization/alias
* @requires collections/set
*/
var Montage = require("../core/core").Montage,
Target = require("../core/target").Target,
Template = require("../core/template").Template,
DocumentResources = require("../core/document-resources").DocumentResources,
Gate = require("../core/gate").Gate,
Promise = require("../core/promise").Promise,
defaultEventManager = require("../core/event/event-manager").defaultEventManager,
Alias = require("../core/serialization/alias").Alias,
logger = require("../core/logger").logger("component"),
drawPerformanceLogger = require("../core/logger").logger("Drawing performance").color.green(),
drawListLogger = require("../core/logger").logger("drawing list").color.blue(),
needsDrawLogger = require("../core/logger").logger("drawing needsDraw").color.violet(),
drawLogger = require("../core/logger").logger("drawing").color.blue(),
WeakMap = require("collections/weak-map"),
Map = require("collections/map"),
Set = require("collections/set");
/**
* @const
* @default
* @type {string}
*/
var ATTR_LE_COMPONENT = "data-montage-le-component",
ATTR_LE_ARG = "data-montage-le-arg",
ATTR_LE_ARG_BEGIN = "data-montage-le-arg-begin",
ATTR_LE_ARG_END = "data-montage-le-arg-end";
function loggerToString (object) {
if (!object) {
return "NIL";
}
return object._montage_metadata.objectName + ":" + Object.hash(object) + " id: " + object.identifier;
}
var CssBasedAnimation = Montage.specialize({
component: {
value: null
},
fromCssClass: {
value: null
},
cssClass: {
value: null
},
toCssClass: {
value: null
},
hasOneFrameDelay: {
value: false
},
_animationAndTransitionProperties: {
value: [
"-webkit-animation",
"-moz-animation",
"-ms-animation",
"animation",
"-webkit-transition",
"-moz-transition",
"-ms-transition",
"transition"
]
},
_emptyArray: {
value: []
},
/**
* Parses the computed style value for a css time property and
* returns an array of numbers representing seconds.
* For example, the time value string "1s, 2s" will return [1, 2]
*/
_parseComputedStyleTimeValue: {
value: function (timeValue) {
var result,
i;
if (typeof timeValue !== "string" || timeValue === "") {
return this._emptyArray;
}
result = timeValue.replace(/s| /g, "").split(",");
for (i = 0; i < result.length; i++) {
result[i] = +result[i];
}
return result;
}
},
/**
* Returns an boundary estimate of the maximum time an element would
* take to complete its css animations and/or transitions.
*/
_getMaxAnimationTime: {
value: function () {
var computedStyle,
durations,
delays,
maxTime = 0,
time,
length,
i, j;
if (this.component && this.component.element) {
computedStyle = global.getComputedStyle(this.component.element);
for (i = 0; i < this._animationAndTransitionProperties.length; i++) {
durations = this._parseComputedStyleTimeValue(
computedStyle.getPropertyValue(this._animationAndTransitionProperties[i] + "-duration")
);
delays = this._parseComputedStyleTimeValue(
computedStyle.getPropertyValue(this._animationAndTransitionProperties[i] + "-delay")
);
length = Math.max(durations.length, delays.length);
for (j = 0; j < length; j++) {
if (typeof durations[j] === "undefined") {
time = durations[0] || 0;
} else {
time = durations[j];
}
if (typeof delays[j] === "undefined") {
time += delays[0] || 0;
} else {
time += delays[j];
}
if (time > maxTime) {
maxTime = time;
}
}
}
if (maxTime > 0) {
// Browsers take several miliseconds since you add the css animation
// or transition property and it really starts. It can range from
// very few miliseconds in desktop to a couple of hundreds in mobile
// devices, so we are adding 300 miliseconds as a safety value that
// should cover the most of the cases.
maxTime += 0.3;
}
}
return maxTime;
}
},
_onAnimationsCompletedTimeout: {
value: null
},
_cancelOnAnimationsCompletedEvent: {
value: function () {
clearTimeout(this._onAnimationsCompletedTimeout);
}
},
_onAnimationsCompleted: {
value: function (callback) {
var maxTime = this._getMaxAnimationTime(),
self;
if (!maxTime) {
callback.call(this);
return;
}
self = this;
this._cancelOnAnimationsCompletedEvent();
this._onAnimationsCompletedTimeout = setTimeout(function () {
callback.call(self);
}, maxTime * 1000);
}
},
_needsToMeasureAnimationTimeOnNextDraw: {
value: false
},
_needsToMeasureAnimationTime: {
value: false
},
handleDidDraw: {
value: function (event) {
if (this._needsToMeasureAnimationTimeOnNextDraw) {
if (this.fromCssClass) {
this.component.classList.remove(this.fromCssClass);
}
if (this.cssClass) {
this.component.classList.add(this.cssClass);
}
if (this.toCssClass) {
this.component.classList.add(this.toCssClass);
}
this._needsToMeasureAnimationTimeOnNextDraw = false;
this._needsToMeasureAnimationTime = true;
this.component.needsDraw = true;
} else {
if (this._needsToMeasureAnimationTime) {
this._onAnimationsCompleted(function () {
if (this._finishedDeferred) {
this._finishedDeferred.resolve();
this._finishedDeferred = null;
}
});
this.component.removeEventListener("didDraw", this, false);
this._needsToMeasureAnimationTime = false;
}
}
}
},
_finishedDeferred: {
value: null
},
finished: {
get: function () {
if (!this._finishedDeferred) {
this._finishedDeferred = Promise.pending();
}
return this._finishedDeferred.promise;
}
},
play: {
value: function () {
if (this.component) {
if (!this._finishedDeferred || this._cancelled) {
this._cancelled = false;
this._finishedDeferred = Promise.pending();
}
this.component.needsDraw = true;
if (this.fromCssClass) {
if (this.fromCssClass) {
this.component.classList.add(this.fromCssClass);
}
if (this.cssClass) {
this.component.classList.remove(this.cssClass);
}
if (this.toCssClass) {
this.component.classList.remove(this.toCssClass);
}
this._needsToMeasureAnimationTime = false;
this._needsToMeasureAnimationTimeOnNextDraw = true;
} else {
if (this.hasOneFrameDelay) {
this._needsToMeasureAnimationTime = false;
this._needsToMeasureAnimationTimeOnNextDraw = true;
} else {
if (this.cssClass) {
this.component.classList.add(this.cssClass);
}
if (this.toCssClass) {
this.component.classList.add(this.toCssClass);
}
this._needsToMeasureAnimationTime = true;
this._needsToMeasureAnimationTimeOnNextDraw = false;
}
}
this.component.addEventListener("didDraw", this, false);
}
}
},
_cancelled: {
value: false
},
cancel: {
value: function () {
this._cancelled = true;
if (this.fromCssClass) {
this.component.classList.remove(this.fromCssClass);
}
if (this.cssClass) {
this.component.classList.remove(this.cssClass);
}
if (this.toCssClass) {
this.component.classList.remove(this.toCssClass);
}
this.component.removeEventListener("didDraw", this, false);
if (this._finishedDeferred) {
this._finishedDeferred.reject();
}
}
}
});
var rootComponent;
/**
* @class Component
* @classdesc Base class for all Montage components.
* @extends Target
*/
var Component = exports.Component = Target.specialize(/** @lends Component.prototype */{
// Virtual Interface
/**
* A human-friendly display title for the component.
* Useful when component is used as a view and a view title is needed, e.g. with a navigation bar.
* Different than {@link Component.identifier}.
*
* @example "User Settings Panel"
*
* @name Component#title
* @property {String}
*/
/**
* An identifier label used to refer to the component in code.
* For example, event handler will call `handleIdentifierAction` which uses this identifier.
* If not defined, will be the same as serialization object's key.
*
* Not to be confused with the human-friendly {@link Component.title}.
*
* @example "userSettingsPanel"
*
* @name Component#identifier
* @property {String}
*/
/**
* Lifecycle hook for when Component's domContent changes.
*
* @name Component#contentWillChange
* @function
* @param {Element} value - The incoming element.
*/
DOM_ARG_ATTRIBUTE: {value: "data-arg"},
drawListLogger: {
value: drawListLogger
},
/**
* A delegate is an object that has helper methods specific to particular
* components.
* For example, a TextField may consult its `deletate`'s
* `shouldBeginEditing()` method, or inform its `delegate` that it
* `didBeginEditing()`.
* Look for details on the documentation of individual components'
* `delegate` properties.
*
* @type {?Object}
* @default null
*/
delegate: {
value: null
},
/**
* This property is populated by the template. It is a map of all the
* instances present in the template's serialization keyed by their label.
*
* @property {boolean} serializable
* @default false
* @property {object} value
* @default null
*/
_templateObjects: {
serializable: false,
value: null
},
templateObjects: {
serializable: false,
get: function() {
if(!this._templateObjects) {
this._templateObjects = Object.create(null);
}
if(!this._setupTemplateObjectsCompleted && this._templateDocumentPart) {
this._setupTemplateObjects(this._templateDocumentPart.objects);
}
return this._templateObjects;
},
set: function(value) {
this._templateObjects = value;
}
},
getResources: {
value: function () {
if (!this._setupTemplateObjectsCompleted && this._templateDocumentPart) {
return this._templateDocumentPart.template.getResources()._resources;
}
}
},
/**
* @private
* @property {Target} value
*/
_nextTarget: {
value: null
},
/**
* The next Target to consider in the event target chain
*
* Currently, components themselves do not allow this chain to be broken;
* setting a component's nextTarget to a falsy value will cause nextTarget
* to resolve as the parentComponent.
*
* To interrupt the propagation path a Target that accepts a falsy
* nextTarget needs to be set at a component's nextTarget.
*
* @param {Target} value
* @returns {Target}
*/
nextTarget: {
get: function () {
return this._nextTarget || this.parentComponent;
},
set: function (value) {
this._nextTarget = value;
}
},
_ownerDocumentPart: {
value: null
},
_templateDocumentPart: {
value: null
},
_domArguments: {
value: void 0
},
_domArgumentNames: {
value: void 0
},
/**
* Dispatch the actionEvent this component is configured to emit upon interaction
* @private
*/
_dispatchActionEvent: {
value: function () {
this.dispatchEvent(this.createActionEvent());
},
enumerable: false
},
/**
* Convenience to create a custom event named "action"
* @function
* @returns and event to dispatch upon interaction
*/
createActionEvent: {
value: function () {
var actionEvent = document.createEvent("CustomEvent");
actionEvent.initCustomEvent("action", true, true, null);
return actionEvent;
}
},
/**
* The gate controlling the canDraw() response of the component.
* @type {Gate}
* @private
*/
canDrawGate: {
get: function () {
if (!this._canDrawGate) {
this._canDrawGate = new Gate().initWithDelegate(this);
this._canDrawGate.setField("componentTreeLoaded", false);
}
return this._canDrawGate;
},
enumerable: false
},
_blockDrawGate: {
value: null
},
/**
* The gate controlling whether the component will ask to draw.
* @type {Gate}
* @private
*/
blockDrawGate: {
enumerable: false,
get: function () {
if (!this._blockDrawGate) {
this._blockDrawGate = new Gate().initWithDelegate(this);
this._blockDrawGate.setField("element", false);
this._blockDrawGate.setField("drawRequested", false);
}
return this._blockDrawGate;
}
},
_firstDraw: {
enumerable: false,
value: true
},
_completedFirstDraw: {
enumerable: false,
value: false
},
originalElement: {
value: null
},
/**
* @private
*/
_element: {
enumerable: false,
value: null
},
/**
* The element of the component as defined in the template.
* ```json
* {
* "component": {
* "values": {
* "element": {"#": "dataMontageId"}
* }
* }
* }
* ```
* DOM arguments can be passed to the component as direct children of the
* element. By default the entire content of the element is considered the
* single DOM argument of the component.
* Multiple arguments can be given by assigning a `data-arg` attribute to
* each element that represents an argument.
*
* ```html
* <div data-montage-id="component">
* <h1 data-arg="title"></h1>
* <div data-arg="content">
* <span data-montage-id="text"></span>
* <div>
* </div>
* ```
*
* If the component has a template then this element is replaced by the
* element that is referenced in its template just before the component
* enters the document.
* ```json
* {
* "owner": {
* "values": {
* "element": {"#": "dataMontageId"}
* }
* }
* }
* ```
*
* The component element has a `component` property that points back to the
* component. This property is specially useful to extrapolate the component
* tree from the DOM tree. It can also be used for debugging purposes, on
* the webkit inspector when an element is selected it's possible to find
* its component by using the `$0.component` command on the console.
*
* The element of a component can only be assigned once, it's not possible
* to change it.
*
* @property {DOMElement}
* @default null
*/
element: {
get: function () {
return this._element;
},
set: function (value) {
if (value === null || value === undefined) {
console.warn("Tried to set element of ", this, " to ", value);
return;
}
if (value.component && value.component !== this) {
throw new Error("Element " + value + " is already assigned to another component: " + value.component);
}
//jshint -W106
if (global._montage_le_flag) {
//jshint +W106
value.setAttribute(ATTR_LE_COMPONENT, Montage.getInfoForObject(this).moduleId);
}
if (this.isDeserializing) {
this.eventManager.registerEventHandlerForElement(this, value);
// if this component has a template and has been already instantiated then assume the value is the template.
if (this._isTemplateInstantiated) {
// this is important for component extension, we don't want to override template element
if (!this._templateElement) {
this._templateElement = value;
}
} else {
this._element = value;
if (!this.blockDrawGate.value && this._element) {
this.blockDrawGate.setField("element", true);
}
}
} else if (!this._firstDraw) {
// If a draw has happened then at some point the element has been set
console.error("Cannot change element of ", this, " after it has been set");
return;
} else {
this.eventManager.registerEventHandlerForElement(this, value);
this._element = value;
if (!this.blockDrawGate.value && this._element) {
this.blockDrawGate.setField("element", true);
}
}
this._initializeClassListFromElement(value);
}
},
getElementId: {
value: function () {
var element = this._element;
if (element) {
return element.getAttribute("data-montage-id");
}
}
},
_initDomArguments: {
value: function () {
var candidates,
domArguments,
name,
node,
element = this.element;
candidates = element.querySelectorAll("*[" + this.DOM_ARG_ATTRIBUTE + "]");
// Need to make sure that we filter dom args that are for nested
// components and not for this component.
if(candidates.length) {
domArguments = {};
}
nextCandidate:
for (var i = 0, candidate; (candidate = candidates[i]); i++) {
node = candidate;
while ((node = node.parentNode) !== element) {
// This candidate is inside another component so skip it.
if (node.component) {
continue nextCandidate;
}
}
this._findAndDetachComponents(candidate);
candidate.parentNode.removeChild(candidate);
name = candidate.getAttribute(this.DOM_ARG_ATTRIBUTE);
candidate.removeAttribute(this.DOM_ARG_ATTRIBUTE);
domArguments[name] = candidate;
}
this._domArguments = domArguments;
}
},
_sharedEmptyArray: {
value: []
},
getDomArgumentNames: {
value: function () {
if (this._domArgumentNames === void 0) {
this._domArgumentNames = this._domArguments ? Object.keys(this._domArguments) : this._sharedEmptyArray;
}
return this._domArgumentNames;
}
},
/**
* This function extracts a DOM argument that was in the element assigned
* to the component.
* The star (`*`) argument refers to the entire content of the element when
* no `data-arg` was given.
*
* When a DOM argument is extracted from a Component it is no longer
* available
*
* @function
* @param {string} name The name of the argument, or `"*"` for the entire
* content.
* @returns the element
*/
extractDomArgument: {
value: function (name) {
if(this._domArguments) {
var argument;
argument = this._domArguments[name];
this._domArguments[name] = null;
return argument;
}
return null;
}
},
/**
* This function is used to get a Dom Argument out of the origin template
* (_ownerDocumentPart) of this component.
* It is not meant to be used with a live DOM, its main purpose it to help
* the TemplateArgumentProvider implementation.
*
* @private
*/
_getTemplateDomArgument: {
value: function (name) {
var candidates,
node,
element,
elementId,
serialization,
labels,
template = this._ownerDocumentPart.template;
element = template.getElementById(this.getElementId());
candidates = element.querySelectorAll("*[" + this.DOM_ARG_ATTRIBUTE + "='" + name + "']");
// Make sure that the argument we find is indeed part of element and
// not an argument from an inner component.
nextCandidate:
for (var i = 0, candidate; (candidate = candidates[i]); i++) {
node = candidate;
while ((node = node.parentNode) !== element) {
elementId = template.getElementId(node);
// Check if this node is an element of a component.
// TODO: Make this operation faster
if (elementId) {
serialization = template.getSerialization();
labels = serialization.getSerializationLabelsWithElements(
elementId);
if (labels.length > 0) {
// This candidate is inside another component so
// skip it.
continue nextCandidate;
}
}
}
return candidate;
}
}
},
/**
* TemplateArgumentProvider implementation
*/
getTemplateArgumentElement: {
value: function (argumentName) {
var ownerModuleId, element, range, argument, label,
template = this._ownerDocumentPart.template;
if (global._montage_le_flag) {
ownerModuleId = this.ownerComponent._montage_metadata.moduleId;
label = this._montage_metadata.label;
}
if (argumentName === "*") {
element = template.getElementById(this.getElementId());
range = template.document.createRange();
range.selectNodeContents(element);
argument = range.cloneContents();
if (global._montage_le_flag && element.children.length > 0) {
this._leTagStarArgument(ownerModuleId, label, argument);
}
} else {
argument = this._getTemplateDomArgument(argumentName).cloneNode(true);
argument.removeAttribute(this.DOM_ARG_ATTRIBUTE);
if (global._montage_le_flag) {
this._leTagNamedArgument(ownerModuleId, label, argument,
argumentName);
}
}
return argument;
}
},
getTemplateArgumentSerialization: {
value: function (elementIds) {
var template = this._ownerDocumentPart.template;
return template._createSerializationWithElementIds(elementIds);
}
},
/**
* @param {string} templatePropertyName "<componentLabel>:<propertyName>"
* @private
*/
resolveTemplateArgumentTemplateProperty: {
value: function (templatePropertyName) {
var ix = templatePropertyName.indexOf(":"),
componentLabel = templatePropertyName.slice(0, ix),
propertyName = templatePropertyName.slice(ix),
documentPart = this._templateDocumentPart,
aliasTemplatePropertyName,
aliasComponent,
alias,
result;
// Check if the template property is referring to this object at all.
if (Montage.getInfoForObject(this).label !== componentLabel) {
return;
}
if (documentPart) {
alias = documentPart.objects[propertyName];
}
if (alias instanceof Alias) {
aliasComponent = documentPart.objects[alias.componentLabel];
// Strip the @ prefix
aliasTemplatePropertyName = alias.value.slice(1);
result = aliasComponent.resolveTemplateArgumentTemplateProperty(aliasTemplatePropertyName);
if (!result) {
result = aliasTemplatePropertyName;
}
}
return result;
}
},
setElementWithParentComponent: {
value: function (element, parent) {
this._alternateParentComponent = parent;
if (this.element !== element) {
this.element = element;
}
}
},
_application: {
value: null
},
// access to the Application object
/**
* Convenience to access the application object.
* @type {Application}
*/
application: {
enumerable: false,
get: function () {
return this._application || (Component.prototype._application = require("../core/application").application);
}
},
/**
* Convenience to access the defaultEventManager object.
* @type {EventManager}
*/
eventManager: {
enumerable: false,
get: function () {
return defaultEventManager;
}
},
/**
* Convenience to access the rootComponent object.
* @type {RootComponent}
*/
rootComponent: {
enumerable: false,
get: function () {
return exports.__root__;
}
},
/**
* @function
* @returns targetElementController
* @private
*/
elementControllerFromEvent: {
enumerable: false,
value: function (event, targetElementController) {
return targetElementController;
}
},
_alternateParentComponent: {
value: null
},
/**
* @private
*/
__parentComponent: {
value: null
},
_parentComponent: {
set: function (value) {
this.__parentComponent = value;
this.dispatchOwnPropertyChange("parentComponent", value);
},
get: function () {
return this.__parentComponent;
}
},
/**
* The parent component is the component that is found by walking up the
* DOM tree, starting at the component's `element`. Each component element
* has a `component` property that points back to the component object, this
* way it's possible to know which component an element represents.
*
* This value is null for the {@link RootComponent}.
*
* @type {Component}
*/
parentComponent: {
enumerable: false,
get: function () {
return this._parentComponent;
}
},
findParentComponent: {
value: function () {
var anElement = this.element,
aParentNode,
eventManager = this.eventManager;
if (anElement) {
while ((aParentNode = anElement.parentNode) && !eventManager.eventHandlerForElement(aParentNode)) {
anElement = aParentNode;
}
return aParentNode ? eventManager.eventHandlerForElement(aParentNode) : this._alternateParentComponent;
}
}
},
querySelectorComponent: {
value: function (selector) {
if (typeof selector !== "string") {
throw "querySelectorComponent: Selector needs to be a string.";
}
// \s*(?:@([^>\s]+)) leftHandOperand [<label>]
// \s*(>)?\s* operator [>] (if undefined it's a space)
// @([^>\s]+) rightHandOperand [<label>]
// (.*) rest
var matches = selector.match(/^\s*(?:@([^>\s]+))?(?:\s*(>)?\s*@([^>\s]+)(.*))?$/);
if (!matches) {
throw "querySelectorComponent: Syntax error \"" + selector + "\"";
}
var childComponents = this.childComponents,
leftHandOperand = matches[1],
operator = matches[2] || " ",
rightHandOperand = matches[3],
rest = matches[4],
found,
i,
childComponent;
if (leftHandOperand) {
rest = rightHandOperand ? "@"+rightHandOperand + rest : "";
for (i = 0, childComponent; (childComponent = childComponents[i]); i++) {
if (leftHandOperand === Montage.getInfoForObject(childComponent).label) {
if (rest) {
return childComponent.querySelectorComponent(rest);
} else {
return childComponent;
}
} else {
found = childComponent.querySelectorComponent(selector);
if (found) {
return found;
}
}
}
} else {
for (i = 0, childComponent; (childComponent = childComponents[i]); i++) {
if (rightHandOperand === Montage.getInfoForObject(childComponent).label) {
if (rest) {
return childComponent.querySelectorComponent(rest);
} else {
return childComponent;
}
}
}
}
return null;
}
},
querySelectorAllComponent: {
value: function (selector, owner) {
if (typeof selector !== "string") {
throw "querySelectorComponent: Selector needs to be a string.";
}
// (@([^>\s]+)? leftHandOperand [<label>]
// \s*(>)?\s* operator [>] (if undefined it's a space)
// @([^>\s]+) rightHandOperand [<label>]
// (.*) rest
var matches = selector.match(/^\s*(?:@([^>\s]+))?(?:\s*(>)?\s*@([^>\s]+)(.*))?$/);
if (!matches) {
throw "querySelectorComponent: Syntax error \"" + selector + "\"";
}
var childComponents = this.childComponents,
leftHandOperand = matches[1],
operator = matches[2] || " ",
rightHandOperand = matches[3],
rest = matches[4],
found = [],
i,
childComponent;
if (leftHandOperand) {
rest = rightHandOperand ? "@"+rightHandOperand + rest : "";
for (i = 0, childComponent; (childComponent = childComponents[i]); i++) {
if (leftHandOperand === Montage.getInfoForObject(childComponent).label && (!owner || owner === childComponent.ownerComponent)) {
if (rest) {
found = found.concat(childComponent.querySelectorAllComponent(rest));
} else {
found.push(childComponent);
}
} else {
found = found.concat(childComponent.querySelectorAllComponent(selector, owner));
}
}
} else {
for (i = 0, childComponent; (childComponent = childComponents[i]); i++) {
if (rightHandOperand === Montage.getInfoForObject(childComponent).label && (!owner || owner === childComponent.ownerComponent)) {
if (rest) {
found = found.concat(childComponent.querySelectorAllComponent(rest, owner));
} else {
found.push(childComponent);
}
}
}
}
return found;
}
},
/**
* The template object of the component.
*
* @type {Template}
* @default null
*/
template: {
enumerable: false,
value: null
},
/**
* Specifies whether the component has an HTML template file associated with
* it.
* @type {boolean}
* @default true
*/
hasTemplate: {
enumerable: false,
value: true
},
/**
* @private
* @type {string}
*/
_templateModuleId: {
serializable: false,
value: null
},
_template: {
value: null
},
// Tree level necessary for ordering drawing re: parent-child
_treeLevel: {
value: 0
},
/**
* @private
* @deprecated
* @function
* @param {Component} childComponent
*/
// TODO update all calls to use addChildComponent and remove this method.
_addChildComponent: {
value: function (childComponent) {
return this.addChildComponent(childComponent);
}
},
addChildComponent: {
value: function (childComponent) {
if (this.childComponents.indexOf(childComponent) === -1) {
this.childComponents.push(childComponent);
childComponent._parentComponent = this;
childComponent._prepareForEnterDocument();
if (childComponent.needsDraw &&
!this.rootComponent.isComponentWaitingNeedsDraw(childComponent)) {
childComponent._addToParentsDrawList();
}
}
childComponent._shouldBuildIn = true;
}
},
attachToParentComponent: {
value: function () {
this.detachFromParentComponent();
this._parentComponent = null;
var parentComponent = this.findParentComponent(),
childComponents,
childComponent;
if (parentComponent) {
// This component's children may have been attached to the
// parent before we were initialized, and so we now need to
// check if any of our parent's children should actually be
// ours.
childComponents = parentComponent.childComponents;
for (var i = 0; (childComponent = childComponents[i]); i++) {
var newParentComponent = childComponent.findParentComponent();
if (newParentComponent === this) {
parentComponent.removeChildComponent(childComponent);
newParentComponent.addChildComponent(childComponent);
}
}
parentComponent.addChildComponent(this);
}
}
},
detachFromParentComponent: {
value: function () {
var parentComponent = this.parentComponent;
if (parentComponent) {
parentComponent.removeChildComponent(this);
}
}
},
removeChildComponent: {
value: function (childComponent) {
var childComponents = this.childComponents,
ix = childComponents.indexOf(childComponent);
if (ix > -1) {
childComponent._exitDocument();
childComponents.splice(ix, 1);
childComponent._parentComponent = null;
childComponent._alternateParentComponent = null;
if (childComponent._addedToDrawList) {
childComponent._addedToDrawList = false;
ix = this._drawList.indexOf(childComponent);
this._drawList.splice(ix, 1);
}
this.rootComponent.removeFromCannotDrawList(childComponent);
}
}
},
_childComponents: {
value: null
},
/**
* The child components of the component.
* This property is readonly and should never be changed.
*
* @type {Array.<Component>}
* @readonly
*/
childComponents: {
enumerable: false,
get: function() {
return this._childComponents || (this._childComponents = []);
}
},
_needsEnterDocument: {
value: false
},
_inDocument: {
value: false
},
__exitDocument: {
value: function () {
if (this._inDocument && typeof this.exitDocument === "function") {
this.exitDocument();
this._inDocument = false;
}
}
},
_exitDocument: {
value: function () {
var traverse;
if (this._needsEnterDocument) {
this._needsEnterDocument = false;
} else {
traverse = function (component) {
var childComponents = component.childComponents,
childComponent;
for (var i = 0; (childComponent = childComponents[i]); i++) {
if (childComponent._isComponentExpanded) {
traverse(childComponent);
}
}
if (component._inDocument) {
component.__exitDocument();
}
};
traverse(this);
}
}
},
/**
* Called when this component is removed from the document's DOM tree.
* @function
*/
exitDocument: {
value: function () {
if (this.isActiveTarget) {
defaultEventManager.activeTarget = this.nextTarget;
}
}
},
_prepareForEnterDocument: {
value: function () {
// On their first draw components will have their needsDraw = true
// when they loadComponentTree.
if (this._firstDraw) {
this._needsEnterDocument = true;
} else {
this.needsDraw = true;
this.traverseComponentTree(function (component) {
if (component._needsEnterDocument) {
return false;
}
component._needsEnterDocument = true;
component.needsDraw = true;
});
}
}
},
/**
* The owner component is the owner of the template form which this
* component was instantiated.
* @type {Component}
* @default null
*/
ownerComponent: {
enumerable: false,
value: null
},
/**
* Unused?
* @private
*/
components: {
enumerable: false,
value: {}
},
_isComponentExpanded: {
enumerable: false,
value: false
},
_isTemplateLoaded: {
enumerable: false,
value: false
},
_isTemplateInstantiated: {
enumerable: false,
value: false
},
/**
* Remove all bindings and starts buffering the needsDraw.
* @function
* @private
*/
cleanupDeletedComponentTree: {
value: function (cancelBindings) {
// Deleting bindings in all cases was causing the symptoms expressed in gh-603
// Until we have a more granular way we shouldn't do this,
// the cancelBindings parameter is a short term fix.
if (cancelBindings) {
this.cancelBindings();
}
this.needsDraw = false;
this.traverseComponentTree(function (component) {
// See above comment
if (cancelBindings) {
component.cancelBindings();
}
component.needsDraw = false;
});
}
},
_newDomContent: {
enumerable: false,
value: null
},
_elementsToAppend: {
value: null
},
domContent: {
serializable: false,
get: function () {
if (this._element) {
return Array.prototype.slice.call(this._element.childNodes, 0);
} else {
return null;
}
},
set: function (value) {
var components,
componentsToAdd = [],
i,
component;
if (!this._elementsToAppend) {
this._elementsToAppend = [];
}
this._newDomContent = value;
this.needsDraw = true;
if (this._newDomContent === null) {
this._shouldClearDomContentOnNextDraw = true;
}
if (typeof this.contentWillChange === "function") {
this.contentWillChange(value);
}
// cleanup current content
components = this.childComponents;
if (value) {
if (!this._componentsPendingBuildOut) {
this._componentsPendingBuildOut = [];
}
for (i = components.length - 1; i >= 0; i--) {
if (this._componentsPendingBuildOut.indexOf(components[i]) === -1) {
this._componentsPendingBuildOut.push(components[i]);
}
}
} else {
this._componentsPendingBuildOut = [];
for (i = components.length - 1; i >= 0; i--) {
components[i]._shouldBuildOut = true;
}
}
if (value instanceof Element) {
this._elementsToAppend.push(value);
this._findAndDetachComponents(value, componentsToAdd);
} else if (value && value[0]) {
for (i = 0; i < value.length; i++) {
this._elementsToAppend.push(value[i]);
this._findAndDetachComponents(value[i], componentsToAdd);
}
}
// not sure if I can rely on _parentComponent to detach the nodes instead of doing one loop for dettach and another to attach...
for (i = 0; (component = componentsToAdd[i]); i++) {
this.addChildComponent(component);
}
}
},
_shouldClearDomContentOnNextDraw: {
value: false
},
_findAndDetachComponents: {
value: function (node, components) {
// TODO: Check if searching the childComponents of the parent
// component can make the search faster..
var component = node.component,
children;
if (!components) {
components = [];
}
if (component) {
component.detachFromParentComponent();
components.push(component);
} else {
// DocumentFragments don't have children so we default to
// childNodes.
children = node.children || node.childNodes;
for (var i = 0, child; (child = children[i]); i++) {
this._findAndDetachComponents(child, components);
}
}
return components;
}
},
// Some components, like the repetition, might use their initial set of
// child components as a template to clone them and instantiate them as the
// real/effective child components.
//
// When this happens the original child components are in a way pointless
// to the application and should not be used.
//
// If other objects get a reference to these child components in the
// template serialization the way to know that they are going to be
// cloned is by checking if one of their parent components has
// its clonesChildComponents set to true.
clonesChildComponents: {
writable: false,
value: false
},
_innerTemplate: {value: null},
innerTemplate: {
serializable: false,
get: function () {
var innerTemplate = this._innerTemplate,
ownerDocumentPart,
ownerTemplate,
elementId,
serialization,
externalObjectLabels,
ownerTemplateObjects,
externalObjects;
if (!innerTemplate) {
ownerDocumentPart = this._ownerDocumentPart;
if (ownerDocumentPart) {
ownerTemplate = ownerDocumentPart.template;
elementId = this.getElementId();
innerTemplate = ownerTemplate.createTemplateFromElementContents(elementId);
serialization = innerTemplate.getSerialization();
externalObjectLabels = serialization.getExternalObjectLabels();
ownerTemplateObjects = ownerDocumentPart.objects;
externalObjects = Object.create(null);
for (var i = 0, label; (label = externalObjectLabels[i]); i++) {
externalObjects[label] = ownerTemplateObjects[label];
}
innerTemplate.setInstances(externalObjects);
this._innerTemplate = innerTemplate;
}
}
return innerTemplate;
},
set: function (value) {
this._innerTemplate = value;
}
},
/**
* This method is called right before draw is called.
* If ```canDraw()``` returns false, then the component is re-added to
* the parent's draw list and draw isn't called.
* This method is evil
*
* @function
* @returns {boolean}
* @private
*/
canDraw: {
value: function () {
return this._canDraw;
}
},
_canDraw: {
get: function () {
return (!this._canDrawGate || this._canDrawGate.value);
},
set: function (value) {
rootComponent.componentCanDraw(this, value);
},
enumerable: false
},
_prepareCanDraw: {
enumerable: false,
value: function _prepareCanDraw() {
if (!this._isComponentTreeLoaded) {
return this.loadComponentTree();
}
}
},
_blocksOwnerComponentDraw: {
value: false
},
_updateOwnerCanDrawGate: {
value: function () {
if (this._blocksOwnerComponentDraw && this.ownerComponent) {
this.ownerComponent.canDrawGate.setField(this, this.canDrawGate.value);
}
}
},
_isComponentTreeLoaded: {
value: false
},
shouldLoadComponentTree: {
value: true
},
_loadComponentTreeDeferred: {value: null},
loadComponentTree: {
value: function loadComponentTree() {
if (!this._loadComponentTreeDeferred) {
this.canDrawGate.setField("componentTreeLoaded", false);
// only put it in the root component's draw list if the
// component has requested to be draw, it's possible to load the
// component tree without asking for a draw.
// What about the hasTemplate check?
if (this.needsDraw || this.hasTemplate) {
this._canDraw = false;
}
var self = this;
this._loadComponentTreeDeferred = this.expandComponent()
.then(function() {
if (self.hasTemplate || self.shouldLoadComponentTree) {
var promises,
childComponents = self._childComponents,
childComponent;
if (childComponents && childComponents.length) {
promises = [];
for (var i = 0; (childComponent = childComponents[i]); i++) {
promises.push(childComponent.loadComponentTree());
}
return Promise.all(promises);
}
return Promise.resolve(null);
}
})
.then(function() {
self._isComponentTreeLoaded = true;
// When the component tree is loaded we need to draw if the
// component needs to have its enterDocument() called.
// This is because we explicitly avoid drawing when we set
// _needsEnterDocument before the first draw because we
// don't want to trigger the draw before its component tree
// is loaded.
if (self._needsEnterDocument) {
self.needsDraw = true;
}
self.canDrawGate.setField("componentTreeLoaded", true);
}).catch(function (error) {
console.error(error);
});
}
return this._loadComponentTreeDeferred;
}
},
/**
* Whenever traverseComponentTree reaches the end of a subtree Component#expandComponent~callback is called.
* @function
* @param {Component#traverseComponentTree~visitor} visitor visitor
* @param {Component#traverseComponentTree~callback} callback callback object
* @private
*/
traverseComponentTree: {value: function traverseComponentTree(visitor, callback) {
var self = this;
function traverse() {
var childComponents = self.childComponents;
var childComponent;
var childLeftCount;
if (visitor) {
// if the visitor returns false stop the traversal for this subtree
if (visitor(self) === false) {
if (callback) {
callback();
}
return;
}
}
if ((childLeftCount = childComponents.length) === 0) {
if (callback) {
callback();
}
return;
}
var visitorFunction = function () {
if (--childLeftCount === 0 && callback) {
callback();
}
};
for (var i = 0; (childComponent = childComponents[i]); i++) {
childComponent.traverseComponentTree(visitor, visitorFunction);
}
}
if (this._isComponentExpanded) {
traverse();
} else if (callback) {
callback();
}
}},
/**
* Visitor function for Component#traverseComponentTree. For every component in the tree, the visitor function is
* called with the current component as an argument.
* If the function returns false then the traversal is stopped for that subtree.
* @function Component#traverseComponentTree~visitor
* @param Component visitedComponent
*/
/**
* @function Component#traverseComponentTree~callback
*/
/**
* @function
* @param {Component#expandComponent~callback} callback TODO
* @private
*/
_expandComponentPromise: {value: null},
expandComponent: {
value: function expandComponent() {
if (!this._expandComponentPromise) {
if (this.hasTemplate) {
var self = this;
this._expandComponentPromise = this._instantiateTemplate().then(function() {
self._isComponentExpanded = true;
self._addTemplateStylesIfNeeded();
self.needsDraw = true;
}).catch(function (error) {
console.error(error);
});
} else {
this._isComponentExpanded = true;
this._expandComponentPromise = Promise.resolve();
}
}
return this._expandComponentPromise;
}
},
_templateObjectDescriptor: {
value: {
enumerable: true,
configurable: true
}
},
_setupTemplateObjects: {
value: function (objects) {
this._templateObjects = this._templateObjects || Object.create(null);
this._addTemplateObjects(objects);
this._setupTemplateObjectsCompleted = true;
return this._templateObjects;
}
},
_setupTemplateObjectsCompleted: {
value: false
},
_addTemplateObjects: {
value: function (objects) {
var label, object,
descriptor = this._templateObjectDescriptor,
templateObjects = this._templateObjects;
/*jshint forin:true */
// TODO add hasOwnProperty to objects
for (label in objects) {
/*jshint forin:false */
object = objects[label];
if (object !== null && object !== undefined) {
if (!Component.prototype.isPrototypeOf(object) || object === this ||
object.parentComponent === this) {
templateObjects[label] = object;
} else {
descriptor.get = this._makeTemplateObjectGetter(this, label, object);
Object.defineProperty(templateObjects, label, descriptor);
}
}
}
}
},
/**
* @private
*/
_makeTemplateObjectGetter: {
value: function (owner, label, object) {
var querySelectorLabel = "@"+label,
isRepeated,
components,
component;
return function templateObjectGetter() {
if (isRepeated) {
return owner.querySelectorAllComponent(querySelectorLabel, owner);
} else {
components = owner.querySelectorAllComponent(querySelectorLabel, owner);
// if there's only one maybe it's not repeated, let's go up
// the tree and found out.
if (components.length === 1) {
component = components[0];
while ((component = component.parentComponent)) {
if (component === owner) {
// we got to the owner without ever hitting a component
// that repeats its child components, we can
// safely recreate this property with a static value
Object.defineProperty(this, label, {
value: components[0]
});
return components[0];
} else if (component.clonesChildComponents) {
break;
}
}
} else if (components.length === 0) {
// We didn't find any in the component tree
// so it was probably removed in the meanwhile.
// We return the one that was in the template
// TODO: need to make sure this component hasn't been disposed.
return object;
}
isRepeated = true;
return components;
}
};
}
},
_instantiateTemplate: {
value: function() {
var self = this;
return this._loadTemplate().then(function(template) {
if (!self._element) {
return Promise.reject(new Error("Cannot instantiate template without an element.", self));
}
var instances = null,
_document = self._element.ownerDocument;
if (!instances) {
instances = Object.create(null);
}
instances.owner = self;
self._isTemplateInstantiated = true;
return template.instantiateWithInstances(instances, _document).then(function (documentPart) {
documentPart.parentDocumentPart = self._ownerDocumentPart;
self._templateDocumentPart = documentPart;
documentPart.fragment = null;
instances = null;
}, function (error) {
throw new Error(template.getBaseUrl() + ":" + error.stack || error);
});
});
}
},
_templateDidLoad: {
value: function (documentPart) {
//If templateObjects was used in serialization's bindings, this._templateObjects will be created empty in the getter. We use this a signal that it needs to
//be setup
//This is call as a delegate by the template before returning the document part from instantiateWithInstances(). Objects in their own templateDidLoad() can
//call templateObjects, so this._templateDocumentPart is needed here.
//This is just set, again, later to the same value in the then() of template.instantiateWithInstances() inside _instantiateTemplate()
this._templateDocumentPart = documentPart;
if(this._templateObjects) {
this._setupTemplateObjects(documentPart.objects);
}
}
},
_loadTemplatePromise: {value: null},
_loadTemplate: {
value: function _loadTemplate() {
var info;
if (!this._loadTemplatePromise) {
var self = this;
info = Montage.getInfoForObject(this);
this._loadTemplatePromise = Template.getTemplateWithModuleId(
this.templateModuleId, info.require)
.then(function(template) {
self._template = template;
self._isTemplateLoaded = true;
return template;
});
}
return this._loadTemplatePromise;
}
},
/**
* @private
* @type {string}
* @default
*/
templateModuleId: {
get: function () {
return this._templateModuleId || this._getDefaultTemplateModuleId();
}
},
_getDefaultTemplateModuleId: {
value: function () {
var templateModuleId,
slashIndex,
moduleId,
info;
info = Montage.getInfoForObject(this);
moduleId = info.moduleId;
slashIndex = moduleId.lastIndexOf("/");
templateModuleId = moduleId + "/" + moduleId.slice(slashIndex === -1 ? 0 : slashIndex+1, -5 /* ".reel".length */) + ".html";
return templateModuleId;
}
},
deserializedFromSerialization: {
value: function () {
this.attachToParentComponent();
}
},
_deserializedFromTemplate: {
value: function (owner, label, documentPart) {
Montage.getInfoForObject(this).label = label;
this._ownerDocumentPart = documentPart;
if (! this.hasOwnProperty("identifier")) {
this.identifier = label;
}
if (!this.ownerComponent) {
if (Component.prototype.isPrototypeOf(owner)) {
this.ownerComponent = owner;
} else {
this.ownerComponent = this.rootComponent;
}
this._updateOwnerCanDrawGate();
}
if (this._needsDrawInDeserialization) {
this.needsDraw = true;
}
}
},
blueprintModuleId: {
serializable: false,
enumerable: false,
get: function () {
return this.objectDescriptorModuleId;
}
},
blueprint: require("../core/core")._objectDescriptorDescriptor,
objectDescriptor: require("../core/core")._objectDescriptorDescriptor,
objectDescriptorModuleId: {
serializable: false,
enumerable: false,
get: function () {
var info = Montage.getInfoForObject(this);
var self = (info && !info.isInstance) ? this : Object.getPrototypeOf(this);
if (!Object.getOwnPropertyDescriptor(self, "_objectDescriptorModuleId") || !self._objectDescriptorModuleId) {
info = Montage.getInfoForObject(self);
var moduleId = info.moduleId,
slashIndex = moduleId.lastIndexOf("/"),
dotIndex = moduleId.lastIndexOf(".");
slashIndex = ( slashIndex === -1 ? 0 : slashIndex + 1 );
dotIndex = ( dotIndex === -1 ? moduleId.length : dotIndex );
dotIndex = ( dotIndex < slashIndex ? moduleId.length : dotIndex );
var objectDescriptorModuleId;
if ((dotIndex < moduleId.length) && ( moduleId.slice(dotIndex, moduleId.length) === ".reel")) {
// We are in a reel
objectDescriptorModuleId = moduleId + "/" + moduleId.slice(slashIndex, dotIndex) + ".meta";
} else {
// We look for the default
objectDescriptorModuleId = moduleId.slice(0, dotIndex) + ".meta";
}
Montage.defineProperty(self, "_objectDescriptorModuleId", {
value: objectDescriptorModuleId
});
}
return self._objectDescriptorModuleId;
}
},
/**
* Callback for the ```canDrawGate```.
* Propagates to the parent and adds the component to the draw list.
* @function
* @param {Gate} gate
* @see Component#canDrawGate
* @private
*/
gateDidBecomeTrue: {
value: function (gate) {
if (gate === this._canDrawGate) {
this._canDraw = true;
this._updateOwnerCanDrawGate();
} else if (gate === this._blockDrawGate) {
rootComponent.componentBlockDraw(this);
this._prepareCanDraw();
}
},
enumerable: false
},
gateDidBecomeFalse: {
value: function (gate) {
if (gate === this._canDrawGate) {
this._updateOwnerCanDrawGate();
}
},
enumerable: false
},
/**
* Gate that controls the _canDraw property. When it becomes true it sets
* _canDraw to true.
* @function
* @returns Gate
* @private
*/
_canDrawGate: {
enumerable: false,
value: null
},
preparedForActivationEvents: {
enumerable: false,
value: false
},
_arrayObjectPool: {
value: {
pool: null,
size: 200,
ix: 0
}
},
_getArray: {
value: function () {
if (!this._arrayObjectPool.pool) {
this._arrayObjectPool.pool = [];
for (var i = 0; i < this._arrayObjectPool.size; i++) {
this._arrayObjectPool.pool[i] = [];
}
}
if (this._arrayObjectPool.ix < this._arrayObjectPool.size) {
return this._arrayObjectPool.pool[this._arrayObjectPool.ix++];
} else {
return [];
}
}
},
_disposeArray: {
value: function (array) {
if (this._arrayObjectPool.ix > 0) {
array.length = 0;
this._arrayObjectPool.pool[--this._arrayObjectPool.ix] = array;
}
}
},
/**
* If needsDraw property returns true this call adds the current component
* instance to the rootComponents draw list.
* Then it iterates on every child component in the component's drawList.
* On everyone of them it calls ```canDraw()```.
* If the result is true, ```_drawIfNeeded()``` is called, otherwise they
* are ignored.
* @private
*/
_drawIfNeeded: {
enumerable: false,
value: function _drawIfNeeded(level) {
var childComponent,
oldDrawList, i, childComponentListLength,
firstDraw = this._firstDraw;
this._treeLevel = level;
if (firstDraw) {
this.originalElement = this.element;
}
if (this.needsDraw) {
this.rootComponent.addToDrawCycle(this);
}
if (this._needsEnterDocument) {
this._needsEnterDocument = false;
this._willEnterDocument();
this._enterDocument(firstDraw);
if (typeof this.enterDocument === "function") {
this.enterDocument(firstDraw);
}
}
if (firstDraw) {
this.originalElement = null;
}
if (this._drawList !== null && this._drawList.length > 0) {
oldDrawList = this._drawList;
this._drawList = this._getArray();
childComponentListLength = oldDrawList.length;
for (i = 0; i < childComponentListLength; i++) {
childComponent = oldDrawList[i];
childComponent._addedToDrawList = false;
if (childComponent.canDraw()) { // TODO if canDraw is false when does needsDraw get reset?
childComponent._drawIfNeeded(level+1);
} else if (drawLogger.isDebug) {
drawLogger.debug(loggerToString(childComponent) + " can't draw.");
}
}
this._disposeArray(oldDrawList);
}
}
},
_updateComponentDom: {
value: function () {
if (this._firstDraw) {
this._prepareForDraw();
if (this._composerList) {
var composer;
// Load any non lazyLoad composers that have been added
for (var i = 0, length = this._composerList.length; i < length; i++) {
composer = this._composerList[i];
if (!composer.lazyLoad) {
this.loadComposer(composer);
}
}
}
// Will we expose a different property, firstDraw, for components to check
this._firstDraw = false;
}
if (this._newDomContent !== null || this._shouldClearDomContentOnNextDraw) {
if (drawLogger.isDebug) {
//jshint -W106
logger.debug("Component content changed: component ", this._montage_metadata.objectName, this.identifier, " newDomContent", this._newDomContent);
//jshint +W106
}
this._performDomContentChanges();
}
}
},
_replaceElementWithTemplate: {
enumerable: false,
value: function () {
var element = this.element,
template = this._templateElement,
attributes = this.element.attributes,
attributeName,
value,
i,
attribute,
templateAttributeValue;
// TODO: get a spec for this, what attributes should we merge?
for (i = 0; (attribute = attributes[i]); i++) {
attributeName = attribute.nodeName;
//jshint -W106
if (global._montage_le_flag && attributeName === ATTR_LE_COMPONENT) {
//jshint +W106
value = attribute.nodeValue;
} else if (attributeName === "id" || attributeName === "data-montage-id") {
value = attribute.nodeValue;
} else {
templateAttributeValue = template.getAttribute(attributeName) || "";
if (templateAttributeValue) {
value = templateAttributeValue +
(attributeName === "style" ? "; " : " ") +
attribute.nodeValue;
} else {
value = attribute.nodeValue;
}
}
template.setAttribute(attributeName, value);
}
this._initializeClassListFromElement(template);
if (element.parentNode) {
element.parentNode.replaceChild(template, element);
} else if (!this._canDrawOutsideDocument) {
console.warn("Warning: Trying to replace element ", element," which has no parentNode");
}
this.eventManager.unregisterEventHandlerForElement(element);
this.eventManager.registerEventHandlerForElement(this, template);
this._element = template;
this._templateElement = null;
// if the DOM content of the component was changed before the
// template has been drawn then we assume that this change is
// meant to set the original content of the component and not to
// replace the entire template with it, that wouldn't make much
// sense.
if (this._newDomContent) {
this._newDomContent = null;
this._shouldClearDomContentOnNextDraw = false;
}
}
},
_addTemplateStylesIfNeeded: {
value: function () {
var part = this._templateDocumentPart;
if (part) {
this.rootComponent.addStyleSheetsFromTemplate(part.template);
}
}
},
_prepareForDraw: {
value: function _prepareForDraw() {
if (logger.isDebug) {
logger.debug(this, "_templateElement: " + this._templateElement);
}
var leTagArguments;
//jshint -W106
if (global._montage_le_flag) {
//jshint +W106
leTagArguments = this.element.children.length > 0;
}
this._initDomArguments();
if (leTagArguments) {
this._leTagArguments();
}
if (this._templateElement) {
this._bindTemplateParametersToArguments();
this._replaceElementWithTemplate();
}
},
enumerable: false
},
_leTagArguments: {
value: function () {
if (this === this.rootComponent) {
return;
}
//jshint -W106
var ownerModuleId = this.ownerComponent._montage_metadata.moduleId;
var label = this._montage_metadata.label;
//jshint +W106
var argumentNames = this.getDomArgumentNames();
if (!argumentNames || argumentNames.length === 0) {
this._leTagStarArgument(ownerModuleId, label, this.element);
} else {
for (var i = 0, name; (name = argumentNames[i]); i++) {
this._leTagNamedArgument(ownerModuleId, label,
this._domArguments[name], name);
}
}
}
},
_getNodeFirstElement: {
value: function (node) {
var element = node.firstElementChild;
if (!element) {
element = node.firstChild;
do {
if (element.nodeType === Node.ELEMENT_NODE) {
break;
}
} while ((element = element.nextSibling));
}
return element;
}
},
_getNodeLastElement: {
value: function (node) {
var element = node.lastElementChild;
if (!element) {
element = node.lastChild;
do {
if (element.nodeType === Node.ELEMENT_NODE) {
break;
}
} while ((element = element.previousSibling));
}
return element;
}
},
_leTagStarArgument: {
value: function (ownerModuleId, label, rootElement) {
var argumentBegin = this._getNodeFirstElement(rootElement);
var argumentEnd = this._getNodeLastElement(rootElement);
argumentBegin.setAttribute(ATTR_LE_ARG_BEGIN,
(argumentBegin.getAttribute(ATTR_LE_ARG_BEGIN)||"") + " " +
ownerModuleId + "," + label);
argumentEnd.setAttribute(ATTR_LE_ARG_END,
(argumentEnd.getAttribute(ATTR_LE_ARG_END)||"") + " " +
ownerModuleId + "," + label);
}
},
_leTagNamedArgument: {
value: function (ownerModuleId, label, element, name) {
element.setAttribute(ATTR_LE_ARG,
ownerModuleId + "," + label + "," + name);
}
},
_bindTemplateParametersToArguments: {
value: function () {
var parameters = this._templateDocumentPart ? this._templateDocumentPart.parameters : null,
templateArguments = this._domArguments,
parameterElement,
argument,
validation,
contents,
components,
range,
component;
if ((validation = this._validateTemplateArguments(templateArguments, parameters))) {
throw validation;
}
for (var key in parameters) {
if (parameters.hasOwnProperty(key)) {
parameterElement = parameters[key];
argument = templateArguments ? templateArguments[key] : void 0;
if ((key === "*") || (key === "each")) {
if (this._element.childElementCount === 0) {
//We're missing an argument, we're going to check if we have a default
if (parameterElement && parameterElement.childElementCount > 0) {
range = this._element.ownerDocument.createRange();
range.selectNodeContents(parameterElement);
parameterElement.parentNode.replaceChild(range.extractContents(), parameterElement);
//Should we re-construct the structure from the default?
// if(!templateArguments) {
// templateArguments = this._domArguments = {"*":};
//
// }
} else {
// throw new Error('No arguments provided for ' +
// this.templateModuleId + '. Arguments needed for data-param: ' +
// key + '.');
//Remove the data-parm="*" element
parameterElement.parentNode.removeChild(parameterElement);
}
} else {
range = this._element.ownerDocument.createRange();
range.selectNodeContents(this._element);
contents = range.extractContents();
}
} else {
contents = argument;
}
if (contents) {
var i, length;
if (contents instanceof Element) {
var classList = parameterElement.classList,
contentsClassList = contents.component ? contents.component.classList : contents.classList;
for (i = 0, length = classList.length; i < length; i++) {
contentsClassList.add(classList[i]);
}
}
components = this._findAndDetachComponents(contents);
parameterElement.parentNode.replaceChild(contents, parameterElement);
for (i = 0; (component = components[i]); i++) {
component.attachToParentComponent();
}
}
}
}
}
},
_validateTemplateArguments: {
value: function (templateArguments, templateParameters) {
var parameterNames = templateParameters ? Object.keys(templateParameters) : void 0,
param;
// If the template does not have parameters it is up to the
// component to use its arguments.
if (!parameterNames || parameterNames.length === 0) {
return;
}
// Arguments for non-existant parameters are not allowed.
// Only the star argument is allowed.
for (param in templateArguments) {
if (param !== "*" && !(param in templateParameters)) {
return new Error('"' + param + '" parameter does ' +
'not exist in ' + this.templateModuleId);
}
}
var elementWithStarParameter = templateParameters["*"];
if (elementWithStarParameter) {
for (param in templateParameters) {
if (param !== "*" && elementWithStarParameter.contains(templateParameters[param])) {
return new Error('"' + param + '" parameter cannot be used within an element with the star parameter');
}
}
}
}
},
/**
* Called by the {@link EventManager} before dispatching a `touchstart` or
* `mousedown`.
*
* The component can implement this method to add event listeners for these
* events before they are dispatched.
* @function
*/
prepareForActivationEvents: {
enumerable: false,
value: null
},
/**
* Called to add event listeners on demand
* @type function
* @private
*/
_prepareForActivationEvents: {
value: function () {
if (typeof this.prepareForActivationEvents === "function") {
this.prepareForActivationEvents();
}
if (this._composerList) {
var composer;
for (var i = 0, length = this._composerList.length; i < length; i++) {
composer = this._composerList[i];
if (composer.lazyLoad) {
this.loadComposer(composer);
}
}
}
this.preparedForActivationEvents = true;
}
},
_performDomContentChanges: {
value: function () {
var contents = this._newDomContent,
element,
elementToAppend,
i;
if (contents || this._shouldClearDomContentOnNextDraw) {
element = this._element;
// Setting the innerHTML to clear the children will not work on
// IE because it modifies the underlying child nodes. Here's the
// test case that shows this issue: http://jsfiddle.net/89X6F/
for (i = element.childNodes.length - 1; i >= 0; i--) {
if (!element.childNodes[i].component) {
element.removeChild(element.childNodes[i]);
}
}
if (this._elementsToAppend) {
while (this._elementsToAppend.length) {
elementToAppend = this._elementsToAppend.shift();
if (!element.contains(elementToAppend)) {
element.appendChild(elementToAppend);
}
}
}
this._newDomContent = null;
if (typeof this.contentDidChange === "function") {
this.contentDidChange();
}
this._shouldClearDomContentOnNextDraw = false;
}
}
},
/**
* @deprecated
* @todo remove
*/
prepareForDraw: {
enumerable: false,
value: null
},
/**
* This method is part of the draw cycle and is the prescribed location for
* components to update its DOM structure or modify its styles.
*
* Components should not read the DOM during this phase of the draw cycle
* as it could force an unwanted reflow from the browser.
*
* @function
* @see http://montagejs.org/docs/Component-draw-cycle.html
*/
draw: {
enumerable: false,
value: Function.noop
},
/**
* This method is part of the draw cycle and it provides the component an
* opportunity to query the DOM for any necessary calculations before
* drawing.
* If the execution of this method sets needsDraw to true on other
* components, those components will be added to the current draw cycle.
*
* Components should not change the DOM during this phase of the draw cycle
* as it could force an unwanted reflow from the browser.
*
* @function
* @see http://montagejs.org/docs/Component-draw-cycle.html
*/
willDraw: {
enumerable: false,
value: null
},
/**
* This method is part of the draw cycle and it provides the component an
* opportunity to query the DOM for any necessary calculations after
* drawing.
* If the execution of this method sets needsDraw to true on other
* components, those components will be added to the current draw cycle.
*
* Components should not change the DOM during this phase of the draw cycle
* as it could force an unwanted reflow from the browser.
*
* @function
* @see http://montagejs.org/docs/Component-draw-cycle.html
*/
didDraw: {
enumerable: false,
value: Function.noop
},
/**
* Records whether or not we have been added to the parent's drawList.
* @private
*/
_addedToDrawList: {
value: false
},
_addToParentsDrawList: {
enumerable: false,
value: function () {
if (!this._addedToDrawList) {
var parentComponent = this._parentComponent;
if (parentComponent) {
parentComponent._addToDrawList(this);
if (this.drawListLogger.isDebug) {
//jshint -W106
this.drawListLogger.debug(loggerToString(this) + " added to " + loggerToString(parentComponent) + "'s drawList");
//jshint +W106
}
} else if (this.drawListLogger.isDebug) {
this.drawListLogger.debug(this, "parentComponent is null");
}
}
}
},
_needsDraw: {
value: false
},
_needsDrawInDeserialization: {
value: false
},
/**
* The purpose of this property is to trigger the adding of the component to
* the draw list. The draw list consists of all the components that will be
* drawn on the next draw cycle.
*
* The draw cycle itself is triggered by the `requestAnimationFrame` API
* where available, otherwise a shim implemented with `setTimeout` is used.
*
* When it happens, the draw cycle will call, in succession, and when they
* exist, the methods: `willDraw`, `draw`, and `didDraw`.
*
* At the end of the draw cycle this property is set back to `false`.
*
* @property {boolean}
* @default false
*/
needsDraw: {
enumerable: false,
get: function () {
return this._needsDraw;
},
set: function (value) {
if (this.isDeserializing) {
// Ignore needsDraw(s) which happen during deserialization
this._needsDrawInDeserialization = true;
return;
}
value = !!value;
if (this._needsDraw !== value) {
if (needsDrawLogger.isDebug) {
//jshint -W106
needsDrawLogger.debug("needsDraw toggled " + value + " for " + this._montage_metadata.objectName);
//jshint +W106
}
this._needsDraw = value;
if (value) {
if (this.canDrawGate.value) {
this._addToParentsDrawList();
} else {
this.blockDrawGate.setField("drawRequested", true);
}
}
}
}
},
/**
* Contains the list of childComponents this instance is reponsible for drawing.
* @private
*/
_drawList: {
value: null
},
__addToDrawList: {
enumerable: false,
value: function (childComponent) {
if (this._drawList === null) {
this._drawList = [childComponent];
childComponent._addedToDrawList = true;
} else {
if (this._drawList.indexOf(childComponent) === -1) {
this._drawList.push(childComponent);
childComponent._addedToDrawList = true;
}
}
}
},
/**
* Adds the passed in child component to the drawList
* If the current instance isn't added to the drawList of its parentComponent, then it adds itself.
* @private
*/
_addToDrawList: {
enumerable: false,
value: function (childComponent) {
this.__addToDrawList(childComponent);
this._addToParentsDrawList();
}
},
_templateElement: {
enumerable: false,
value: null
},
// Pointer Claiming
/**
* Ask this component to surrender the specified pointer to the
* demandingComponent.
*
* The component can decide whether or not it should do this given the
* pointer and demandingComponent involved.
*
* Some components may decide not to surrender control ever, while others
* may do so in certain situations.
*
* Returns true if the pointer was surrendered, false otherwise.
*
* The demandingComponent is responsible for claiming the surrendered
* pointer if it desires.
*
* @function
* @param {string} pointer The `pointerIdentifier` that the demanding
* component is asking this component to surrender
* @param {Object} demandingComponent The component that is asking this
* component to surrender the specified pointer
* @returns {boolean} true
*/
surrenderPointer: {
value: function (pointer, demandingComponent) {
return true;
}
},
// Composers
/**
* Variable to track this component's associated composers
* @private
*/
_composerList: {
value: null,
serializable: false
},
composerList: {
get: function () {
if (!this._composerList) {
this._composerList = [];
}
return this._composerList;
},
serializable: false
},
/**
* Adds the passed in composer to the component's composer list.
* @function
* @param {Composer} composer
*/
addComposer: { // What if the same composer instance is added to more than one component?
value: function (composer) {
this.addComposerForElement(composer, composer.element);
}
},
/**
* Adds the passed in composer to the component's composer list and
* sets the element of the composer to the passed in element.
* @function
* @param {Composer} composer
* @param {Element} element
*/
addComposerForElement: {
value: function (composer, element) {
composer.component = this;
composer.element = element;
this.composerList.push(composer);
if (!this._firstDraw) { // prepareForDraw has already happened so do the loading here
if (!composer.lazyLoad) {
this.loadComposer(composer);
} else if (this.preparedForActivationEvents) { // even though it's lazyLoad prepareForActivationEvents has already happened
this.loadComposer(composer);
}
}
}
},
/**
* Load a Composer
* @function
* @param {Composer} composer
*/
loadComposer: {
value: function (composer) {
if (this._composerList && this._composerList.indexOf(composer) > -1) {
Target.prototype.loadComposer.call(this, composer);
}
}
},
/**
* Unload a Composer
* @function
* @param {Composer} composer
*/
unloadComposer: {
value: function (composer) {
if (this._composerList && this._composerList.indexOf(composer) > -1) {
Target.prototype.unloadComposer.call(this, composer);
}
}
},
/**
* Adds the passed in composer to the list of composers which will have their
* frame method called during the next draw cycle. It causes a draw cycle to be scheduled
* iff one has not already been scheduled.
* @function
* @param {Composer} composer
*/
scheduleComposer: {
value: function (composer) {
this.rootComponent.addToComposerList(composer);
}
},
/**
* Removes the passed in composer from this component's composer list. It takes care
* of calling the composers unload method before removing it from the list.
* @function
* @param {Composer} composer
*/
removeComposer: {
value: function (composer) {
if (this._composerList) {
for (var i = 0, length = this._composerList.length; i < length; i++) {
if (this._composerList[i] === composer) {
this.unloadComposer(this._composerList[i]);
this._composerList.splice(i, 1);
break;
}
}
}
}
},
/**
* A convenience method for removing all composers from a component. This method
* is responsible for calling unload on each composer before removing it.
* @function
*/
clearAllComposers: {
value: function () {
if (this._composerList) {
var composerList = this._composerList;
for (var i = 0, length = composerList.length; i < length; i++) {
this.unloadComposer(composerList[i]);
}
composerList.length = 0;
}
}
},
/**
* The localizer for this component
* @type {Localizer}
* @default null
*/
localizer: {
value: null
},
_waitForLocalizerMessages: {
value: false
},
/**
* Whether to wait for the localizer to load messages before drawing.
* Make sure to set the [localizer]{@link Component#localizer} before
* setting to ```true```.
*
* @type {boolean}
* @default false
* @example
* // require localizer
* var defaultLocalizer = localizer.defaultLocalizer,
* _ = defaultLocalizer.localizeSync.bind(defaultLocalizer);
*
* exports.Main = Component.specialize( {
*
* constructor: {
* value: function () {
* this.localizer = defaultLocalizer;
* this.waitForLocalizerMessages = true;
* }
* },
*
* // ...
*
* // no draw happens until the localizer's messages have been loaded
* enterDocument: {
* value: function (firstTime) {
* if (firstTime) {
* this._greeting = _("hello", "Hello {name}!");
* }
* }
* },
* draw: {
* value: function () {
* // this is for illustration only. This example is simple enough that
* // you should use a localizations binding
* this._element.textContent = this._greeting({name: this.name});
* }
* }
* }
*/
waitForLocalizerMessages: {
enumerable: false,
get: function () {
return this._waitForLocalizerMessages;
},
set: function (value) {
if (this._waitForLocalizerMessages !== value) {
if (value === true && !this.localizer.messages) {
if (!this.localizer) {
throw "Cannot wait for messages on localizer if it is not set";
}
this._waitForLocalizerMessages = true;
logger.debug(this, "waiting for messages from localizer");
this.canDrawGate.setField("messages", false);
var self = this;
this.localizer.messagesPromise.then(function(messages) {
if (logger.isDebug) {
logger.debug(self, "got messages from localizer");
}
self.canDrawGate.setField("messages", true);
});
} else {
this._waitForLocalizerMessages = false;
this.canDrawGate.setField("messages", true);
}
}
}
},
//
// Attribute Handling
//
/**
* Stores values that need to be set on the element. Cleared each draw cycle.
* @private
*/
__elementAttributeValues: {
value: null
},
_elementAttributeValues: {
get: function() {
return this.__elementAttributeValues || (this.__elementAttributeValues = {});
}
},
/**
* Stores the descriptors of the properties that can be set on this control
* @private
*/
_elementAttributeDescriptors: {
value: null
},
_getElementAttributeDescriptor: {
value: function (attributeName) {
var attributeDescriptor, instance = this;
// walk up the prototype chain from the instance to NativeControl's prototype
// if _elementAttributeDescriptors is falsy, stop.
while(instance && instance._elementAttributeDescriptors) {
attributeDescriptor = instance._elementAttributeDescriptors[attributeName];
if(attributeDescriptor) {
break;
} else {
instance = Object.getPrototypeOf(instance);
}
}
return attributeDescriptor;
}
},
__shouldBuildIn: {
value: true
},
_shouldBuildIn: {
get: function () {
return this.__shouldBuildIn;
},
set: function (value) {
var index;
value = !!value;
this.__shouldBuildIn = value;
if (value) {
if (this.parentComponent && this.parentComponent._componentsPendingBuildOut) {
index = this.parentComponent._componentsPendingBuildOut.indexOf(this);
if (index !== -1) {
this.parentComponent._componentsPendingBuildOut.splice(index, 1);
}
}
this._shouldBuildOut = false;
if (this._inDocument) {
this._buildIn();
}
}
}
},
__shouldBuildOut: {
value: false
},
_shouldBuildOut: {
get: function () {
return this.__shouldBuildOut;
},
set: function (value) {
value = !!value;
this.__shouldBuildOut = value;
if (value) {
this._shouldBuildIn = false;
if (this._inDocument) {
this._buildOut();
}
}
}
},
buildInInitialAnimation: {
get: function () {
var animation = null;
if (this._activeBuildInAnimation && typeof this._activeBuildInAnimation === "object") {
if (typeof this._activeBuildInAnimation.cssClass !== "undefined") {
animation = new CssBasedAnimation();
animation.component = this;
animation.fromCssClass = this._activeBuildInAnimation.fromCssClass;
animation.cssClass = this._activeBuildInAnimation.cssClass;
animation.toCssClass = this._activeBuildInAnimation.toCssClass;
}
}
return animation;
}
},
buildInSwitchAnimation: {
get: function () {
var animation = null;
if (this._activeBuildInAnimation && typeof this._activeBuildInAnimation === "object") {
if (typeof this._activeBuildInAnimation.cssClass !== "undefined") {
animation = new CssBasedAnimation();
animation.component = this;
animation.cssClass = this._activeBuildInAnimation.cssClass;
animation.toCssClass = this._activeBuildInAnimation.toCssClass;
}
}
return animation;
}
},
buildOutInitialAnimation: {
get: function () {
var animation = null;
if (this._activeBuildOutAnimation && typeof this._activeBuildOutAnimation === "object") {
if (typeof this._activeBuildOutAnimation.cssClass !== "undefined") {
animation = new CssBasedAnimation();
animation.component = this;
animation.hasOneFrameDelay = true;
animation.fromCssClass = this._activeBuildOutAnimation.fromCssClass;
animation.cssClass = this._activeBuildOutAnimation.cssClass;
animation.toCssClass = this._activeBuildOutAnimation.toCssClass;
}
}
return animation;
}
},
buildOutSwitchAnimation: {
get: function () {
var animation = null;
if (this._activeBuildOutAnimation && typeof this._activeBuildOutAnimation === "object") {
if (typeof this._activeBuildOutAnimation.cssClass !== "undefined") {
animation = new CssBasedAnimation();
animation.component = this;
animation.cssClass = this._activeBuildOutAnimation.cssClass;
animation.toCssClass = this._activeBuildOutAnimation.toCssClass;
}
}
return animation;
}
},
buildInAnimation: {
value: null
},
buildOutAnimation: {
value: null
},
buildInAnimationOverride: {
value: null
},
buildOutAnimationOverride: {
value: null
},
_activeBuildInAnimation: {
value: null
},
_activeBuildOutAnimation: {
value: null
},
_updateActiveBuildAnimations: {
value: function () {
if (this.buildInAnimationOverride) {
this._activeBuildInAnimation = this.buildInAnimationOverride;
} else {
this._activeBuildInAnimation = this.buildInAnimation;
}
if (this.buildOutAnimationOverride) {
this._activeBuildOutAnimation = this.buildOutAnimationOverride;
} else {
this._activeBuildOutAnimation = this.buildOutAnimation;
}
}
},
_currentBuildAnimation: {
value: null
},
_buildIn: {
value: function () {
var self = this;
if (this._currentBuildAnimation) {
this._currentBuildAnimation.cancel();
}
if (this._element && this._element.parentNode && this._element.parentNode.component) {
if (this._isElementAttachedToParent) {
this._currentBuildAnimation = this.buildInSwitchAnimation;
} else {
this._updateActiveBuildAnimations();
this._currentBuildAnimation = this.buildInInitialAnimation;
}
if (this._currentBuildAnimation) {
this._currentBuildAnimation.play();
this._currentBuildAnimation.finished.then(function () {
self._currentBuildAnimation.cancel();
self._currentBuildAnimation = null;
self.dispatchEventNamed("buildInEnd", true, true);
}, function () {});
}
}
}
},
_buildOut: {
value: function () {
var self = this;
if (this._currentBuildAnimation) {
this._currentBuildAnimation.cancel();
this._currentBuildAnimation = this.buildOutSwitchAnimation;
} else {
this._updateActiveBuildAnimations();
this._currentBuildAnimation = this.buildOutInitialAnimation;
}
if (this._element && this._element.parentNode && this._element.parentNode.component) {
if (this._currentBuildAnimation) {
this._currentBuildAnimation.play();
this._currentBuildAnimation.finished.then(function () {
var parent = self.parentComponent;
self._currentBuildAnimation.cancel();
self._currentBuildAnimation = null;
self.detachFromParentComponent();
self.buildInAnimationOverride = null;
self.buildOutAnimationOverride = null;
if (self._element.parentNode.component) {
self._element.parentNode.removeChild(self._element);
}
self._isElementAttachedToParent = false;
parent.dispatchEventNamed("buildOutEnd", true, true);
}, function () {});
} else {
this.detachFromParentComponent();
this.buildInAnimationOverride = null;
this.buildOutAnimationOverride = null;
if (this._isElementAttachedToParent) {
if (this._element.parentNode && this._element.parentNode.component) {
this._element.parentNode.removeChild(this._element);
}
this._isElementAttachedToParent = false;
}
}
}
}
},
_willEnterDocument: {
value: function () {
var event;
event = new CustomEvent("willEnterDocument", {
details: {
component: this
},
bubbles: false
});
this.dispatchEvent(event);
this._inDocument = true;
if (this.parentComponent) {
this.parentComponent._childWillEnterDocument();
}
if (this.__shouldBuildIn) {
this._buildIn();
}
this._isElementAttachedToParent = true;
if (this.__shouldBuildOut) {
this._buildOut();
}
}
},
_childWillEnterDocument: {
value: function () {
if (this._componentsPendingBuildOut) {
while (this._componentsPendingBuildOut.length) {
this._componentsPendingBuildOut.pop()._shouldBuildOut = true;
}
}
}
},
/**
* This function is called when the component element is added to the
* document's DOM tree.
*
* @function Component#enterDocument
* @param {boolean} firstTime `true` if it's the first time the component
* enters the document.
*/
// callbacks
_enterDocument: {
value: function (firstTime) {
var originalElement;
if (firstTime) {
// The element is now ready, so we can read the attributes that
// have been set on it.
originalElement = this.originalElement;
var attributes, i, length, name, value, attributeName, descriptor;
attributes = originalElement.attributes;
if (attributes) {
length = attributes.length;
for (i=0; i < length; i++) {
name = attributes[i].name;
value = attributes[i].value;
descriptor = this._getElementAttributeDescriptor(name, this);
// check if this attribute from the markup is a well-defined attribute of the component
if (descriptor || (typeof this[name] !== 'undefined')) {
// only set the value if a value has not already been set by binding
if (typeof this._elementAttributeValues[name] === 'undefined') {
this._elementAttributeValues[name] = value;
if(this[name] === null || this[name] === undefined) {
this[name] = value;
}
}
}
}
}
// textContent is a special case since it isn't an attribute
descriptor = this._getElementAttributeDescriptor('textContent', this);
if(descriptor) {
// check if this element has textContent
var textContent = originalElement.textContent;
if (typeof this._elementAttributeValues.textContent === 'undefined') {
this._elementAttributeValues.textContent = textContent;
if (this.textContent === null || this.textContent === undefined) {
this.textContent = textContent;
}
}
}
// Set defaults for any properties that weren't serialised or set
// as attributes on the element.
//Benoit: This shouldn't be needed on each instance if properly set on the prototype TODO #memory #performance improvement
if (this._elementAttributeDescriptors) {
for (attributeName in this._elementAttributeDescriptors) {
if (this._elementAttributeDescriptors.hasOwnProperty(attributeName)) {
descriptor = this._elementAttributeDescriptors[attributeName];
var _name = "_"+attributeName;
if ((this[_name] === null) && descriptor !== null && "value" in descriptor) {
this[_name] = descriptor.value;
}
}
}
}
}
}
},
/**
* @private
* @function
*/
_draw: {
value: function () {
var element = this.element,
descriptor;
//Buffered/deferred element attribute values
if(this.__elementAttributeValues !== null) {
for(var attributeName in this._elementAttributeValues) {
if(this._elementAttributeValues.hasOwnProperty(attributeName)) {
var value = this[attributeName];
descriptor = this._getElementAttributeDescriptor(attributeName, this);
if(descriptor) {
if(descriptor.dataType === 'boolean') {
if(value === true) {
element[attributeName] = true;
element.setAttribute(attributeName, attributeName.toLowerCase());
} else {
element[attributeName] = false;
element.removeAttribute(attributeName);
}
} else {
if(typeof value !== 'undefined') {
if(attributeName === 'textContent') {
element.textContent = value;
} else {
//https://developer.mozilla.org/en/DOM/element.setAttribute
element.setAttribute(attributeName, value);
}
}
}
}
delete this._elementAttributeValues[attributeName];
}
}
}
// classList
this._drawClassListIntoComponent();
}
},
/**
* @private
*/
_classList: {
value: null
},
_classListDirty: {
value: false
},
/**
* The classList of the component's element, the purpose is to mimic the
* element's API but to also respects the draw cycle.
*
* It can also be bound to by binding each class as a property.
* example to toggle the complete class:
*
* ```json
* "classList.has('complete')" : { "<-" : "@owner.isComplete"}
* ```
*
* @returns {Set}
*/
classList: {
get: function () {
if (this._classList === null) {
this._classList = new Set();
this._subscribeToToClassListChanges();
this._initializeClassListFromElement(this.element);
}
return this._classList;
}
},
/**
* @private
*/
_initializeClassListFromElement: {
value: function (element) {
if (element && element.classList && element.classList.length > 0) {
// important to initializae the classList first, so that the listener doesn't get installed.
if (!this._classList) {
// we don't want to subscribe then unsubscribe and subscribe again to the ClassList Changes,
// So we don't access to the getter of the property classList.
this._classList = new Set();
} else {
if (this._unsubscribeToClassListChanges) {
this._unsubscribeToClassListChanges();
}
}
var classList = element.classList;
for (var i = 0, length = classList.length; i < length; i++) {
this._classList.add(classList[i]);
}
this._subscribeToToClassListChanges();
}
}
},
/**
* @private
*/
_unsubscribeToClassListChanges: {
value: null
},
/**
* @private
*/
_subscribeToToClassListChanges: {
value: function () {
this._unsubscribeToClassListChanges = this._classList.addRangeChangeListener(this, "classList");
}
},
handleClassListRangeChange: {
value: function (plus, minus) {
this._classListDirty = true;
this.needsDraw = true;
}
},
_drawClassListIntoComponent: {
value: function () {
if (this._classListDirty) {
var elementClassList = this.element.classList,
classList = this._classList;
for (var i = 0, ii = elementClassList.length, className; i < ii; i++) {
className = elementClassList.item(i);
if (!classList.has(className)) {
elementClassList.remove(className);
i--;
ii--;
}
}
this._classList.forEach(function (cssClass) {
elementClassList.add(cssClass);
});
this._classListDirty = false;
}
}
},
dispose: {
value: function () {
this.cancelBindings();
this.detachFromParentComponent();
if (this._element) {
defaultEventManager.unregisterEventHandlerForElement(this._element);
this._element = null;
}
this.childComponents.forEach(function (component) {
component.dispose();
});
}
}
},{
/**
* Add the specified properties as properties of this component.
* @function
* @param {object} properties An object that contains the properties you want to add.
* @private
*/
//TODO, this should be renamed addAttributeProperties
addAttributes: {
value: function (properties) {
var i, descriptor, property, object;
this.prototype._elementAttributeDescriptors = properties;
for(property in properties) {
if(properties.hasOwnProperty(property)) {
object = properties[property];
// Make sure that the descriptor is of the correct form.
if(object === null || typeof object === "string") {
descriptor = {value: object, dataType: "string"};
properties[property] = descriptor;
} else {
descriptor = object;
}
// Only add the internal property, and getter and setter if
// they don't already exist.
if(typeof this[property] === 'undefined') {
this.defineAttribute(property, descriptor);
}
}
}
}
},
//TODO, this should be renamed attributePropertySetter
defineAttributeSetter: {
value: function (name, _name, descriptor) {
return (function (name, attributeName, setter) {
return function (value, fromInput) {
var descriptor = this._getElementAttributeDescriptor(name, this);
// if requested dataType is boolean (eg: checked, readonly etc)
// coerce the value to boolean
if(descriptor && "boolean" === descriptor.dataType) {
value = ( (value || value === "") ? true : false);
}
// If the set value is different to the current one,
// update it here, and set it to be updated on the
// element in the next draw cycle.
if(typeof value !== 'undefined' && (this[attributeName] !== value)) {
if (setter) {
setter.call(this, value);
} else {
this[attributeName] = value;
}
this._elementAttributeValues[name] = value;
if (!fromInput) {
this.needsDraw = true;
}
}
};
}(name, _name, descriptor.set));
}
},
//TODO, this should be renamed attributePropertySetter
defineAttributeGetter: {
value: function (_name) {
return (function (attributeName) {
return function () {
return this[attributeName];
};
}(_name));
}
},
/**
* Adds a property to the component with the specified name.
* This method is used internally by the framework convert a DOM element's
* standard attributes into bindable properties.
* It creates an accessor property (getter/setter) with the same name as
* the specified property, as well as a "backing" data property whose name
* is prepended with an underscore (_).
* The backing variable is assigned the value from the property descriptor.
* For example, if the name "title" is passed as the first parameter, a
* "title" accessor property is created as well a data property named
* "_title".
* @function
* @param {string} name The property name to add.
* @param {Object} descriptor An object that specifies the new properties default attributes such as configurable and enumerable.
* @private
*/
//https://github.com/kangax/html-minifier/issues/63 for a list of boolean attributes
//TODO, this should be renamed defineAttributeProperty
defineAttribute: {
value: function (name, descriptor) {
descriptor = descriptor || {};
var _name = '_' + name;
var newDescriptor = {
configurable: (typeof descriptor.configurable === 'undefined') ? true: descriptor.configurable,
enumerable: (typeof descriptor.enumerable === 'undefined') ? true: descriptor.enumerable,
set: this.defineAttributeSetter(name, _name, descriptor),
get: descriptor.get || this.defineAttributeGetter(_name)
};
// Define _ property
// TODO this.constructor.defineProperty
if(!this.prototype.hasOwnProperty(_name)) {
Montage.defineProperty(this.prototype, _name, {value: descriptor.value});
}
// Define property getter and setter
Montage.defineProperty(this.prototype, name, newDescriptor);
}
}
});
/**
* @class RootComponent
* @extends Component
*/
var RootComponent = Component.specialize( /** @lends RootComponent.prototype */{
constructor: {
value: function RootComponent() {
this._drawTree = this._drawTree.bind(this);
this._readyToDrawListIndex = new Map();
this._addedStyleSheetsByTemplate = new WeakMap();
}
},
/**
* @private
* @function
* @returns itself
*/
init: {
value: function () {
return this;
}
},
needsDraw: {
enumerable: true,
get: function () {
return !!this._needsDraw;
},
set: function (value) {
if (this._needsDraw !== value) {
this._needsDraw = !!value;
if (value) {
var childComponents = this.childComponents;
//jshint -W106
for (var i = 0, childComponent; (childComponent = childComponents[i]); i++) {
if (needsDrawLogger.isDebug) {
needsDrawLogger.debug(this, "needsDraw = true for: " + childComponent._montage_metadata.exportedSymbol);
}
childComponent.needsDraw = true;
}
//jshint +W106
}
}
}
},
canDrawGate: {
get: function () {
return this._canDrawGate || (this._canDrawGate = new Gate().initWithDelegate(this));
}
},
_clearNeedsDrawTimeOut: {
value: null
},
_needsDrawList: {
value: []
},
_cannotDrawList: {
value: null
},
/**
* @function
* @param {Object} component
*/
componentBlockDraw: {
value: function (component) {
this._cannotDrawList = (this._cannotDrawList ? this._cannotDrawList : new Set());
this._cannotDrawList.add(component);
if (this._clearNeedsDrawTimeOut) {
clearTimeout(this._clearNeedsDrawTimeOut);
this._clearNeedsDrawTimeOut = null;
}
}
},
// TODO: implement this with a flag on the component
isComponentWaitingNeedsDraw: {
value: function (component) {
return this._cannotDrawList.has(component) ||
this._needsDrawList.indexOf(component) >= 0;
}
},
/**
* @function
* @param {Object} component
* @param {number} value
*/
componentCanDraw: {
value: function (component, value) {
if (value) {
if (!this._cannotDrawList) {
return;
}
this._cannotDrawList.delete(component);
this._needsDrawList.push(component);
if (this._cannotDrawList.size === 0 && this._needsDrawList.length > 0) {
if (!this._clearNeedsDrawTimeOut) {
var self = this;
// Wait to clear the needsDraw list as components could be loaded synchronously
this._clearNeedsDrawTimeOut = setTimeout(function () {
self._clearNeedsDrawList();
}, 0);
}
}
} else {
if (this._clearNeedsDrawTimeOut) {
clearTimeout(this._clearNeedsDrawTimeOut);
this._clearNeedsDrawTimeOut = null;
}
}
}
},
_clearNeedsDrawList: {
value: function () {
var component, i, length, needsDrawList = this._needsDrawList;
length = needsDrawList.length;
for (i = 0; i < length; i++) {
component = needsDrawList[i];
if (component.needsDraw ||
// Maybe the component doesn't need to draw but has child
// components that do.
(component._drawList && component._drawList.length > 0)) {
component._addToParentsDrawList();
}
}
this._clearNeedsDrawTimeOut = null;
needsDrawList.length = 0;
}
},
/**
* @function
* @param {Component} componentId
*/
removeFromCannotDrawList: {
value: function (component) {
if (!this._cannotDrawList) {
return;
}
this._cannotDrawList.delete(component);
if (this._cannotDrawList.size === 0 && this._needsDrawList.length > 0) {
if (!this._clearNeedsDrawTimeOut) {
var self = this;
this._clearNeedsDrawTimeOut = setTimeout(function () {
self._clearNeedsDrawList();
}, 0);
}
}
}
},
_cancelDrawIfScheduled: {
value: function () {
var requestedAnimationFrame = this.requestedAnimationFrame,
cancelAnimationFrame = this.cancelAnimationFrame;
if (requestedAnimationFrame !== null) {
if (!this._frameTime) { // Only cancel it is not already in a drawTree call
if (logger.isDebug) {
logger.debug(this, "clearing draw");
}
if (cancelAnimationFrame) {
cancelAnimationFrame(requestedAnimationFrame);
} else {
clearTimeout(requestedAnimationFrame);
}
this.requestedAnimationFrame = null;
}
}
}
},
/**
* Adds the passed in child component to the drawList.
* @private
*/
_addToDrawList: {
value: function (childComponent) {
this.__addToDrawList(childComponent);
if (this.drawListLogger.isDebug) {
this.drawListLogger.debug(this, this.canDrawGate.value, this.requestedAnimationFrame);
}
this.drawTree();
},
enumerable: false
},
/**
* Adds the passed in composer to the list of composers to be executed
* in the next draw cycle and requests a draw cycle if one has not been
* requested yet.
* @function
* @param {Composer} composer
*/
addToComposerList: {
value: function (composer) {
this.composerList.push(composer);
if (drawLogger.isDebug) {
drawLogger.debug(this, composer, "Added to composer list");
}
// If a draw is already in progress this.drawTree() will not schedule another one, so track
// that a composer requested a draw in case a new draw does need to be scheduled when the
// current loop is done
this._scheduleComposerRequest = true;
this.drawTree();
}
},
// Create a second composer list so that the lists can be swapped during a draw instead of creating a new array every time
composerListSwap: {
get: function () {
if (!this._composerListSwap) {
this._composerListSwap = [];
}
return this._composerListSwap;
}
},
_composerListSwap: {
value: null
},
/*
* Flag to track if a composer is requesting a draw
* @private
*/
_scheduleComposerRequest: {
value: false
},
/**
* The value returned by requestAnimationFrame.
* If a request has been scheduled but not run yet, else null.
* @private
* @type {number}
* @default null
*/
requestedAnimationFrame: {
value: null,
enumerable: false
},
/**
* @private
* @function
*/
requestAnimationFrame: {
value: (global.requestAnimationFrame || global.webkitRequestAnimationFrame ||
global.mozRequestAnimationFrame || global.msRequestAnimationFrame || setTimeout),
enumerable: false
},
/**
* @private
* @function
*/
cancelAnimationFrame: {
value: (global.cancelAnimationFrame || global.webkitCancelAnimationFrame ||
global.mozCancelAnimationFrame || global.msCancelAnimationFrame || clearTimeout),
enumerable: false
},
/**
* Set to the current time of the frame while drawing is in progress.
* The frame time is either supplied by the requestAnimationFrame callback if available in the browser, or by using Date.now if it is a setTimeout.
* @private
*/
_frameTime: {
value: null
},
/**
* oldSource and diff are used to detect DOM modifications outside of the
* draw loop, but only if drawLogger.isDebug is true.
* @private
*/
_oldSource: {
value: null
},
_diff: {
// Written by John Resig. Used under the Creative Commons Attribution 2.5 License.
// http://ejohn.org/projects/javascript-diff-algorithm/
value: function ( o, n ) {
var ns = {},
os = {};
function isNullOrUndefined(o) {
return o === undefined || o === null;
}
for (var i = 0; i < n.length; i++ ) {
if (isNullOrUndefined(ns[n[i]])) {
ns[n[i]] = {
rows: [],
o: null
};
}
ns[n[i]].rows.push( i );
}
for (i = 0; i < o.length; i++ ) {
if (isNullOrUndefined(os[o[i]])) {
os[o[i]] = {
rows: [],
n: null
};
}
os[o[i]].rows.push(i);
}
for (i in ns ) {
if (
ns[i].rows.length === 1 &&
!isNullOrUndefined(os[i]) &&
os[i].rows.length === 1
) {
n[ns[i].rows[0]] = {
text: n[ns[i].rows[0]],
row: os[i].rows[0]
};
o[ os[i].rows[0] ] = {
text: o[ os[i].rows[0] ],
row: ns[i].rows[0]
};
}
}
for (i = 0; i < n.length - 1; i++ ) {
if (
!isNullOrUndefined(n[i].text) && isNullOrUndefined(n[i+1].text) &&
n[i].row + 1 < o.length && isNullOrUndefined(o[ n[i].row + 1 ].text) &&
n[i+1] === o[ n[i].row + 1 ]
) {
n[i+1] = { text: n[i+1], row: n[i].row + 1 };
o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
}
}
for (i = n.length - 1; i > 0; i-- ) {
if (
!isNullOrUndefined(n[i].text) && isNullOrUndefined(n[i - 1].text) &&
n[i].row > 0 && isNullOrUndefined(o[ n[i].row - 1].text) &&
n[i - 1] === o[ n[i].row - 1 ]
) {
n[i - 1] = {
text: n[i - 1],
row: n[i].row - 1
};
o[n[i].row-1] = {
text: o[n[i].row-1],
row: i - 1
};
}
}
return {
o: o,
n: n
};
}
},
/**
* @private
*/
_previousDrawDate: {
enumerable: false,
value: 0
},
/**
* @private
*/
_documentResources: {
value: null
},
/**
* @private
*/
_needsStylesheetsDraw: {
value: false
},
/**
* @private
*/
_stylesheets: {
value: []
},
/**
* @function
*/
addStylesheet: {
value: function (style) {
this._stylesheets.push(style);
this._needsStylesheetsDraw = true;
}
},
_addedStyleSheetsByTemplate: {
value: null
},
addStyleSheetsFromTemplate: {
value: function(template) {
if(!this._addedStyleSheetsByTemplate.has(template)) {
var resources = template.getResources(),
ownerDocument = this.element.ownerDocument,
styles = resources.createStylesForDocument(ownerDocument);
for (var i = 0, style; (style = styles[i]); i++) {
this.addStylesheet(style);
}
this._addedStyleSheetsByTemplate.set(template,true);
}
}
},
__bufferDocumentFragment: {
value: null,
},
_bufferDocumentFragment: {
get: function() {
return this.__bufferDocumentFragment || ( this.__bufferDocumentFragment = this._element.ownerDocument.createDocumentFragment());
}
},
/**
* @private
*/
drawStylesheets: {
value: function () {
var documentResources = this._documentResources,
stylesheets = this._stylesheets,
stylesheet,
documentHead = documentResources._document.head,
bufferDocumentFragment = this._bufferDocumentFragment;
while ((stylesheet = stylesheets.shift())) {
documentResources.addStyle(stylesheet,bufferDocumentFragment);
}
documentHead.insertBefore(bufferDocumentFragment, documentHead.firstChild);
this._needsStylesheetsDraw = false;
}
},
/**
* @private
*/
drawTree: {
value: function drawTree() {
if (this.requestedAnimationFrame === null) { // 0 is a valid requestedAnimationFrame value
var requestAnimationFrame = this.requestAnimationFrame;
if (requestAnimationFrame) {
this.requestedAnimationFrame = requestAnimationFrame.call(window, this._drawTree);
} else {
// Shim based in Erik Möller's code at
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
var currentDate = Date.now(),
miliseconds = 17 - currentDate + this._previousDrawDate;
if (miliseconds < 0) {
miliseconds = 0;
}
this.requestedAnimationFrame = setTimeout(this._drawTree, miliseconds);
this._previousDrawDate = currentDate + miliseconds;
}
this._scheduleComposerRequest = false;
}
},
enumerable: false
},
_drawTree: {
value: function (timestamp) {
var drawPerformanceStartTime;
// Add all stylesheets needed by the components since last
// draw.
if (this._needsStylesheetsDraw) {
this.drawStylesheets();
}
// Wait for all stylesheets to be loaded, do not proceeed
// with the draw cycle until all needed stylesheets are
// ready.
// We need to do this because adding the stylesheets won't
// make them immediately available for styling even if the
// file is already loaded.
if (!this._documentResources.areStylesLoaded) {
if (drawPerformanceLogger.isDebug) {
console.log("Draw Cycle Waiting Stylesheets: ", this._documentResources._expectedStyles.length);
}
this.requestedAnimationFrame = null;
this.drawTree();
return;
}
if (drawPerformanceLogger.isDebug) {
if (window.performance) {
drawPerformanceStartTime = window.performance.now();
} else {
drawPerformanceStartTime = Date.now();
}
}
this._frameTime = (timestamp ? timestamp : Date.now());
if (this._clearNeedsDrawTimeOut) {
this._clearNeedsDrawList();
}
if (drawLogger.isDebug) {
// Detect any DOM modification since the previous draw
var newSource = document.body.innerHTML;
if (this._oldSource && newSource !== this._oldSource) {
var warning = ["DOM modified outside of the draw loop"];
var out = this._diff(this._oldSource.split("\n"), newSource.split("\n"));
for (var i = 0; i < out.n.length; i++) {
if (out.n[i].text === undefined || out.n[i].text === null) {
warning.push('+ ' + out.n[i]);
} else {
for (var n = out.n[i].row + 1; n < out.o.length && (out.o[n].text === undefined || out.o[n].text === null); n++) {
warning.push('- ' + out.o[n]);
}
}
}
console.warn(warning.join("\n"));
}
console.group((timestamp ? drawLogger.toTimeString(new Date(timestamp)) + " " : "") + "Draw Fired");
}
this.drawIfNeeded();
if (drawPerformanceLogger.isDebug) {
var drawPerformanceEndTime;
if (window.performance) {
drawPerformanceEndTime = window.performance.now();
} else {
drawPerformanceEndTime = Date.now();
}
console.log("Draw Cycle Time: ",
drawPerformanceEndTime - drawPerformanceStartTime,
", Components: ", this._lastDrawComponentsCount);
}
if (drawLogger.isDebug) {
console.groupEnd();
this._oldSource = document.body.innerHTML;
}
this._frameTime = null;
if (this._scheduleComposerRequest) {
this.drawTree();
}
}
},
/**
* @private
*/
_readyToDrawList: {
enumerable: false,
value: []
},
/**
* @private
*/
_readyToDrawListIndex: {
enumerable: false,
value: null
},
/**
* @function
* @param {Component} component Component to add
*/
addToDrawCycle: {
value: function (component) {
var needsDrawListIndex = this._readyToDrawListIndex, length, composer;
if (needsDrawListIndex.has(component)) {
// Requesting a draw of a component that has already been drawn in the current cycle
if (drawLogger.isDebug) {
if(this !== rootComponent) {
drawLogger.debug(loggerToString(this) + " added to the draw cycle twice, this should not happen.");
}
}
return;
}
this._readyToDrawList.push(component);
this._readyToDrawListIndex.set(component, true);
component._updateComponentDom();
}
},
_lastDrawComponentsCount: {
value: null
},
_sortByLevel: {
value: function (component1, component2) {
return component1._treeLevel - component2._treeLevel;
}
},
/**
* @private
* @function
* @returns Boolean true if all the components that needed to draw have drawn
*/
drawIfNeeded:{
value: function drawIfNeeded() {
var needsDrawList = this._readyToDrawList, component, i, j, start = 0, firstDrawEvent,
composerList = this._composerList, composer, composerListLength,
isDrawLoggerDebug = drawLogger.isDebug;
needsDrawList.length = 0;
this._readyToDrawListIndex.clear();
// Process the composers first so that any components that need to be newly drawn due to composer changes
// get added in this cycle
if (composerList && (composerListLength = composerList.length) > 0) {
this._composerList = this.composerListSwap; // Swap between two arrays instead of creating a new array each draw cycle
for (i = 0; i < composerListLength; i++) {
composer = composerList[i];
composer.needsFrame = false;
composer.frame(this._frameTime);
}
composerList.length = 0;
this._composerListSwap = composerList;
}
this._drawIfNeeded(0);
j = needsDrawList.length;
//
// willDraw
//
if (isDrawLoggerDebug) {
console.groupCollapsed("willDraw - " + needsDrawList.length +
(needsDrawList.length > 1 ? " components." : " component."));
}
while (start < j) {
for (i = start; i < j; i++) {
component = needsDrawList[i];
if (typeof component.willDraw === "function") {
component.willDraw(this._frameTime);
}
if (isDrawLoggerDebug) {
drawLogger.debug("Level " + component._treeLevel + " " + loggerToString(component));
}
}
this._drawIfNeeded(0);
start = j;
j = needsDrawList.length;
}
//
// draw
//
if (isDrawLoggerDebug) {
console.groupEnd();
console.group("draw - " + needsDrawList.length +
(needsDrawList.length > 1 ? " components." : " component."));
}
// Sort the needsDraw list so that any newly added items are drawn in the correct order re: parent-child
needsDrawList.sort(this._sortByLevel);
for (i = 0; i < j; i++) {
component = needsDrawList[i];
component.needsDraw = false;
}
this.requestedAnimationFrame = null; // Allow a needsDraw called during a draw to schedule the next draw
// TODO: add the possibility to display = "none" the body during development (IKXARIA-3631).
for (i = j-1; i >= 0; i--) {
component = needsDrawList[i];
component._draw(this._frameTime);
component.draw(this._frameTime);
if (isDrawLoggerDebug) {
drawLogger.debug("Level " + component._treeLevel + " " + loggerToString(component));
}
}
//
// didDraw
//
if (isDrawLoggerDebug) {
console.groupEnd();
console.groupCollapsed("didDraw - " + needsDrawList.length +
(needsDrawList.length > 1 ? " components." : " component."));
}
if (!this._didDrawEvent) {
this._didDrawEvent = new CustomEvent("didDraw", {
bubbles: false
});
}
for (i = 0; i < j; i++) {
component = needsDrawList[i];
component.dispatchEvent(this._didDrawEvent);
component.didDraw(this._frameTime);
if (!component._completedFirstDraw) {
firstDrawEvent = document.createEvent("CustomEvent");
firstDrawEvent.initCustomEvent("firstDraw", true, false, null);
component.dispatchEvent(firstDrawEvent);
component._completedFirstDraw = true;
}
if (isDrawLoggerDebug) {
drawLogger.debug("Level " + component._treeLevel + " " + loggerToString(component));
}
}
//Now root Component:
if (!this._completedFirstDraw) {
firstDrawEvent = document.createEvent("CustomEvent");
firstDrawEvent.initCustomEvent("firstDraw", true, false, null);
this.dispatchEvent(firstDrawEvent);
this._completedFirstDraw = true;
}
if (isDrawLoggerDebug) {
console.groupEnd();
}
if (drawPerformanceLogger.isDebug) {
this._lastDrawComponentsCount = needsDrawList.length;
}
return !!needsDrawList.length;
}
},
/**
* @private
* @type {DOMElement}
* @default null
*/
element: {
get:function () {
return this._element;
},
set:function (document) {
defaultEventManager.registerEventHandlerForElement(this, document);
this._element = document.documentElement;
this._documentResources = DocumentResources.getInstanceForDocument(document);
}
}
});
exports.__root__ = rootComponent = new RootComponent().init();
//https://github.com/kangax/html-minifier/issues/63
//http://www.w3.org/TR/html-markup/global-attributes.html
Component.addAttributes( /** @lends module:montage/ui/control.Control# */ {
/**
Specifies the shortcut key(s) that gives focuses to or activates the element.
@see {@link http://www.w3.org/TR/html5/editing.html#the-accesskey-attribute}
@type {string}
@default null
*/
accesskey: null,
/**
Specifies if the content is editable or not. Valid values are "true", "false", and "inherit".
@see {@link http://www.w3.org/TR/html5/editing.html#contenteditable}
@type {string}
@default null
*/
contenteditable: null,
/**
Specifies the ID of a <code>menu</code> element in the DOM to use as the element's context menu.
@see {@link http://www.w3.org/TR/html5/interactive-elements.html#attr-contextmenu}
@type {string}
@default null
*/
contextmenu: null,
/**
Specifies the elements element's text directionality. Valid values are "ltr", "rtl", and "auto".
@see {@link http://www.w3.org/TR/html5/elements.html#the-dir-attribute}
@type {string}
@default null
*/
dir: null,
/**
Specifies if the element is draggable. Valid values are "true", "false", and "auto".
@type {string}
@default null
@see {@link http://www.w3.org/TR/html5/dnd.html#the-draggable-attribute}
*/
draggable: null,
/**
Specifies the behavior that's taken when an item is dropped on the element. Valid values are "copy", "move", and "link".
@type {string}
@see {@link http://www.w3.org/TR/html5/dnd.html#the-dropzone-attribute}
*/
dropzone: null,
/**
When specified on an element, it indicates that the element should not be displayed.
@type {boolean}
@default false
*/
hidden: {dataType: 'boolean'},
//id: null,
/**
Specifies the primary language for the element's contents and for any of the element's attributes that contain text.
@type {string}
@default null
@see {@link http://www.w3.org/TR/html5/elements.html#attr-lang}
*/
lang: null,
/**
Specifies if element should have its spelling and grammar checked by the browser. Valid values are "true", "false".
@type {string}
@default null
@see {@link http://www.w3.org/TR/html5/editing.html#attr-spellcheck}
*/
spellcheck: null,
// /**
// The CSS styling attribute.
// @type {string}
// @default null
// @see {@link http://www.w3.org/TR/html5/elements.html#the-style-attribute}
// */
// style: null,
/**
Specifies the relative order of the element for the purposes of sequential focus navigation.
@type {number}
@default null
@see {@link http://www.w3.org/TR/html5/editing.html#attr-tabindex}
*/
tabindex: null,
/**
Specifies advisory information about the element, used as a tooltip when hovering over the element, and other purposes.
@type {string}
@default null
@see {@link http://www.w3.org/TR/html5/elements.html#the-title-attribute}
*/
title: null
});