var Montage = require("./core").Montage,
Deserializer = require("core/serialization/deserializer/montage-deserializer").MontageDeserializer,
DocumentPart = require("./document-part").DocumentPart,
DocumentResources = require("./document-resources").DocumentResources,
Serialization = require("./serialization/serialization").Serialization,
MontageLabeler = require("./serialization/serializer/montage-labeler").MontageLabeler,
Promise = require("./promise").Promise,
URL = require("./mini-url"),
logger = require("./logger").logger("template"),
defaultEventManager = require("./event/event-manager").defaultEventManager,
defaultApplication;
/**
* @class Template
* @extends Montage
*/
var Template = Montage.specialize( /** @lends Template# */ {
_SERIALIZATION_SCRIPT_TYPE: {value: "text/montage-serialization"},
_ELEMENT_ID_ATTRIBUTE: {value: "data-montage-id"},
PARAM_ATTRIBUTE: {value: "data-param"},
_require: {value: null},
_resources: {value: null},
_baseUrl: {value: null},
_instances: {value: null},
_metadata: {value: null},
_objectsString: {value: null},
objectsString: {
get: function () {
return this._objectsString;
},
set: function (value) {
this._objectsString = value;
if (this._serialization) {
this._serialization.initWithString(value);
}
// Invalidate the deserializer cache since there's a new
// serialization in town.
this.__deserializer = null;
}
},
// Deserializer cache
__deserializer: {value: null},
_deserializer: {
get: function () {
var deserializer = this.__deserializer,
metadata,
requires;
if (!deserializer) {
metadata = this._metadata;
if (metadata) {
requires = Object.create(null);
/* jshint forin: true */
for (var label in metadata) {
/* jshint forin: false */
requires[label] = metadata[label].require;
}
}
deserializer = new Deserializer().init(this.objectsString,
this._require, requires);
this.__deserializer = deserializer;
}
return deserializer;
}
},
getDeserializer: {
value: function () {
return this._deserializer;
}
},
_serialization: {
value: null
},
getSerialization: {
value: function () {
var serialiation = this._serialization;
if (!serialiation) {
serialiation = this._serialization = new Serialization();
serialiation.initWithString(this.objectsString);
}
return serialiation;
}
},
_isDirty: {
value: false
},
isDirty: {
get: function () {
return this._isDirty;
},
set: function (value) {
if (this._isDirty !== value) {
this._isDirty = value;
this.clearTemplateFromElementContentsCache();
}
}
},
/**
* Object that knows how to refresh the contents of the template when it's
* dirty. Expected to implement the refreshTemplate(template) function.
*/
refresher: {
value: null
},
_document: {
value: null
},
document: {
get: function () {
if (this._isDirty) {
this.refresh();
}
return this._document;
},
set: function (value) {
this._document = value;
}
},
/**
* Initializes the Template with an empty document.
*
* @function
* @param {require} _require The require function used to load modules when
* a template is instantiated.
*/
initWithRequire: {
value: function (_require) {
this._require = _require;
this.document = this.createHtmlDocumentWithHtml("");
this.objectsString = "";
return this;
}
},
/**
* Initializes the Template with a document.
*
* @function
* @param {HTMLDocument} _document The document to be used as a template.
* @param {require} _require The require function used to load modules when
* a template is instantiated.
* @returns {Promise} A promise for the proper initialization of the
* template.
*/
initWithDocument: {
value: function (_document, _require) {
var self = this;
this._require = _require;
this.setDocument(_document);
return this.getObjectsString(_document)
.then(function (objectsString) {
self.objectsString = objectsString;
return self;
});
}
},
/**
* Initializes the Template with an HTML string.
*
* @function
* @param {HTMLDocument} html The HTML string to be used as a template.
* @param {require} _require The require function used to load modules when
* a template is instantiated.
* @returns {Promise} A promise for the proper initialization of the
* template.
*/
initWithHtml: {
value: function (html, _require) {
var self = this;
this._require = _require;
this.document = this.createHtmlDocumentWithHtml(html);
return this.getObjectsString(this.document)
.then(function (objectsString) {
self.objectsString = objectsString;
return self;
});
}
},
/**
* Initializes the Template with Objects and a DocumentFragment to be
* used as the body of the document.
*
* @function
* @param {Object} objects A JSON'able representation of the objects of the
* template.
* @param {DocumentFragment} html The HTML string to be used as the body.
* @param {require} _require The require function used to load modules when
* a template is instantiated.
* @returns {Promise} A promise for the proper initialization of the
* template.
*/
initWithObjectsAndDocumentFragment: {
value: function (objects, documentFragment, _require) {
this._require = _require;
this.document = this.createHtmlDocumentWithHtml("");
this.document.body.appendChild(
this.document.importNode(documentFragment, true)
);
this.setObjects(objects);
return this;
}
},
/**
* Initializes the Template with the HTML document at the module id.
*
* @function
* @param {string} moduleId The module id of the HTML page to load.
* @param {require} _require The require function used to load modules when
* a template is instantiated.
* @returns {Promise} A promise for the proper initialization of the
* template.
*/
initWithModuleId: {
value: function (moduleId, _require) {
var self = this;
this._require = _require;
return this.createHtmlDocumentWithModuleId(moduleId, _require)
.then(function (_document) {
var baseUrl = _require(moduleId).directory;
self.document = _document;
self.setBaseUrl(baseUrl);
return self.getObjectsString(_document)
.then(function (objectsString) {
self.objectsString = objectsString;
return self;
});
});
}
},
clone: {
value: function () {
var clonedTemplate = new Template();
clonedTemplate._require = this._require;
clonedTemplate._baseUrl = this._baseUrl;
clonedTemplate.setDocument(this.document);
clonedTemplate.objectsString = this.objectsString;
clonedTemplate._instances = Object.clone(this._instances, 1);
return clonedTemplate;
}
},
instantiate: {
value: function (targetDocument) {
return this.instantiateWithInstances(null, targetDocument);
}
},
/**
* @param instances {Object} The instances to use in the serialization
* section of the template, when given they will be used instead of
* creating a new object. It's dictionary where the keys are the
* labels and the values the instances.
* @param targetDocument {Document} The document used to create the markup
* resultant of the instantiation.
*/
instantiateWithInstances: {
value: function (instances, targetDocument) {
var self = this,
fragment,
part = new DocumentPart(),
templateObjects,
templateParameters;
instances = instances || this._instances;
fragment = this._createMarkupDocumentFragment(targetDocument);
templateParameters = this._getParameters(fragment);
part.initWithTemplateAndFragment(this, fragment);
part.startActingAsTopComponent();
part.parameters = templateParameters;
templateObjects = this._createTemplateObjects(instances);
return this._instantiateObjects(templateObjects, fragment)
.then(function (objects) {
var resources = self.getResources();
if (!resources.resourcesLoaded() && resources.hasResources()) {
// Start preloading the resources as soon as possible, no
// need to wait for them as the draw cycle will take care
// of that when loading the stylesheets into the document.
resources.loadResources(targetDocument);
}
part.objects = objects;
self._invokeDelegates(part, instances);
part.stopActingAsTopComponent();
return part;
});
}
},
_objectsInstantiationOptimized: {
value: false
},
_optimizeObjectsInstantiationPromise: {
value: null
},
/**
* @returns {undefined|Promise} A promise if there are objects to optimize,
* nothing otherwise.
*/
_optimizeObjectsInstantiation: {
value: function () {
var self = this,
promise;
if (!this._objectsInstantiationOptimized) {
if (!this._optimizeObjectsInstantiationPromise) {
promise = this._deserializer.preloadModules();
if (promise) {
this._optimizeObjectsInstantiationPromise = promise
.then(function () {
self._objectsInstantiationOptimized = true;
});
} else {
this._objectsInstantiationOptimized = true;
}
}
return this._optimizeObjectsInstantiationPromise;
}
}
},
setBaseUrl: {
value: function (baseUrl) {
this._baseUrl = baseUrl;
}
},
getBaseUrl: {
value: function () {
return this._baseUrl;
}
},
getResources: {
value: function () {
var resources = this._resources;
if (!resources) {
resources = this._resources = new exports.TemplateResources();
resources.initWithTemplate(this);
}
return resources;
}
},
/**
* Creates the object instances to be passed to the deserialization.
* It takes instances and augments it with "application" and "template".
*
* @param {Object} instances The instances object.
* @returns {Object} The object with instances and application and template.
*/
_createTemplateObjects: {
value: function (instances) {
var templateObjects = Object.create(instances || null);
if (typeof defaultApplication === "undefined") {
defaultApplication = require("./application").application;
}
templateObjects.application = defaultApplication;
templateObjects.template = this;
return templateObjects;
}
},
_instantiateObjects: {
value: function (instances, fragment) {
var deserializer = this._deserializer,
optimizationPromise;
if (Promise.is(deserializer)) {
return deserializer; // Promise return (error raised while deserializing)
}
optimizationPromise = this._optimizeObjectsInstantiation();
if (optimizationPromise) {
return optimizationPromise.then(function () {
return deserializer.deserialize(instances, fragment);
});
} else {
return deserializer.deserialize(instances, fragment);
}
}
},
_createMarkupDocumentFragment: {
value: function (targetDocument) {
var fragment = targetDocument.createDocumentFragment(),
nodes = this.document.body.childNodes;
for (var i = 0, ii = nodes.length; i < ii; i++) {
fragment.appendChild(
targetDocument.importNode(nodes[i], true)
);
}
return fragment;
}
},
getParameterName: {
value: function (element) {
return element.getAttribute(this.PARAM_ATTRIBUTE);
}
},
getParameters: {
value: function () {
return this._getParameters(this.document.body);
}
},
_getParameters: {
value: function (rootElement) {
var elements = rootElement.querySelectorAll("*[" + this.PARAM_ATTRIBUTE + "]"),
elementsCount = elements.length,
element,
parameterName,
parameters = {};
for (var i = 0; i < elementsCount; i++) {
element = elements[i];
parameterName = this.getParameterName(element);
if (parameterName in parameters) {
throw new Error('The parameter "' + parameterName + '" is' +
' declared more than once in ' + this.getBaseUrl() +
'.');
}
parameters[parameterName] = element;
}
return parameters;
}
},
hasParameters: {
value: function () {
return !!this.document.querySelector("*[" + this.PARAM_ATTRIBUTE + "]");
}
},
_invokeDelegates: {
value: function (documentPart, instances) {
var objects = documentPart.objects,
object,
owner = objects.owner || instances && instances.owner,
objectOwner,
objectLabel;
/* jshint forin: true */
for (var label in objects) {
/* jshint forin: false */
// Don't call delegate methods on objects that were passed to
// the instantiation.
if (instances && label in instances) {
continue;
}
object = objects[label];
// getObjectOwner will take into account metadata that might
// have been set for this object. Objects in the serialization
// of the template might have different owners. This is true
// when an object in the serialization is the result of a
// data-param that was expanded using arguments from an external
// template.
objectOwner = this._getObjectOwner(label, owner);
objectLabel = this._getObjectLabel(label);
if (object) {
if (typeof object._deserializedFromTemplate === "function") {
object._deserializedFromTemplate(objectOwner, objectLabel, documentPart);
}
if (typeof object.deserializedFromTemplate === "function") {
object.deserializedFromTemplate(objectOwner, objectLabel, documentPart);
}
}
}
if (owner) {
var serialization = this.getSerialization();
// Don't call delegate methods on external objects
if (!serialization.isExternalObject("owner")) {
if (typeof owner._templateDidLoad === "function") {
owner._templateDidLoad(documentPart);
}
if (typeof owner.templateDidLoad === "function") {
owner.templateDidLoad(documentPart);
}
}
}
}
},
/**
* Sets the instances to use when instantiating the objects of the template.
* These instances will always be used when instantiating the template
* unless a different set of instances is passed in
* instantiateWithInstances().
*
* @function
* @param {Object} instances The objects' instances.
*/
setInstances: {
value: function (instances) {
this._instances = instances;
}
},
getInstances: {
value: function () {
return this._instances;
}
},
setObjects: {
value: function (objects) {
// TODO: use Serializer.formatSerialization(object|string)
this.objectsString = JSON.stringify(objects, null, 4);
}
},
/**
* Add metadata to specific objects of the serialization.
*
* @param {string} label The label of the object in the serialization.
* @param {Require} _require The require function to be used when loading
* the module.
* @param {string} effectiveLabel An alternative label to be given to the
* object.
* @param {Object} owner The owner object to be given to the object.
*/
setObjectMetadata: {
value: function (label, _require, effectiveLabel, owner) {
var metadata = this._metadata;
if (!metadata) {
this._metadata = metadata = Object.create(null);
}
metadata[label] = {
"require": _require,
"label": effectiveLabel,
"owner": owner
};
// Invalidate the deserializer cache since we need to setup new
// requires.
this.__deserializer = null;
}
},
getObjectMetadata: {
value: function (label) {
var metadata = this._metadata;
if (metadata && label in metadata) {
return metadata[label];
} else {
return {
"require": this._require,
"label": label
};
}
}
},
_getObjectOwner: {
value: function (label, defaultOwner) {
var objectOwner,
metadata = this._metadata;
if (metadata && label in metadata) {
objectOwner = metadata[label].owner;
} else {
objectOwner = defaultOwner;
}
return objectOwner;
}
},
_getObjectLabel: {
value: function (label) {
var objectLabel,
metadata = this._metadata;
if (metadata && label in metadata) {
objectLabel = metadata[label].label;
} else {
objectLabel = label;
}
return objectLabel;
}
},
/**
* Uses the document markup as the base of the template markup.
*
* @function
* @param {HTMLDocument} doc The document.
* @returns {Promise} A promise for the proper initialization of the
* document.
*/
setDocument: {
value: function(_document) {
this.document = this.cloneHtmlDocument(_document);
this.clearTemplateFromElementContentsCache();
}
},
/**
* Searches for objects in the document.
* The objects string can live as an inline script in the document or as an
* external resource that needs to be loaded.
*
* @function
* @param {HTMLDocument} doc The document with the objects string.
* @returns {Promise} A promise for the objects string, null if not
* found.
*/
getObjectsString: {
value: function (doc) {
var objectsString;
objectsString = this.getInlineObjectsString(doc);
if (objectsString === null) {
return this.getExternalObjectsString(doc);
} else {
return Promise.resolve(objectsString);
}
}
},
/**
* Searches for an inline objects string in a document and returns it if
* found.
*
* @function
* @param {HTMLDocument} doc The document with the objects string.
* @returns {?String} The objects string or null if not found.
*/
getInlineObjectsString: {
value: function (doc) {
var selector = "script[type='" + this._SERIALIZATION_SCRIPT_TYPE + "']",
script = doc.querySelector(selector);
if (script) {
return script.textContent;
} else {
return null;
}
}
},
/**
* Searches for an external objects file in a document and returns its
* contents if found.
*
* @function
* @param {string} doc The document to search.
* @returns {Promise} A promise to the contents of the objects file or null
* if none found.
*/
getExternalObjectsString: {
value: function (doc) {
var link = doc.querySelector('link[rel="serialization"]'),
deferred;
if (link) {
// TODO use core/request
deferred = new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
var url = link.getAttribute("href");
req.open("GET", url);
req.addEventListener("load", function(event) {
var req = event.target;
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(
new Error("Unable to retrive '" + url + "', code status: " + req.status)
);
}
}, false);
req.addEventListener("error", function(event) {
reject(
new Error("Unable to retrive '" + url + "' with error: " + event.error + ".")
);
}, false);
req.send();
});
return deferred;
} else {
return Promise.resolve(null);
}
}
},
createHtmlDocumentWithHtml: {
value: function (html, baseURI) {
var htmlDocument = document.implementation.createHTMLDocument("");
if (html) {
htmlDocument.documentElement.innerHTML = html;
if (baseURI) {
this.normalizeRelativeUrls(htmlDocument, baseURI);
}
}
return htmlDocument;
}
},
cloneHtmlDocument: {
value: function (htmlDocument) {
/*
* Temporary fix for the function createHtmlDocumentWithHtml.
*
* Indeed, innerHTML does not allow us to have a document with some invalid HTML code,
* that could belong to an iteration's template for example.
*
* Such as:
* <body><tr></tr></body> --> <body></body>
*
* Todo Plus, The DOMImplementation.createHTMLDocument() method has a parameter optional `title`
* https://dom.spec.whatwg.org/#domimplementation
*
* Removing it will improve the performance, but it is not optional for IE.
*
*/
var clonedDocument = document.implementation.createHTMLDocument(""),
baseURI = htmlDocument.baseURI || htmlDocument.URL;
clonedDocument.replaceChild(
clonedDocument.importNode(htmlDocument.documentElement, true),
clonedDocument.documentElement
);
this.normalizeRelativeUrls(clonedDocument, baseURI);
return clonedDocument;
}
},
createHtmlDocumentWithModuleId: {
value: function (moduleId, _require) {
var self = this;
if (typeof _require !== "function") {
return Promise.reject(
new Error("Missing 'require' function to load module '" + moduleId + "'.")
);
}
return _require.async(moduleId).then(function (exports) {
return self.createHtmlDocumentWithHtml(exports.content, exports.directory);
});
}
},
/**
* Removes all artifacts related to objects string
*/
_removeObjects: {
value: function (doc) {
var selector = "script[type='" + this._SERIALIZATION_SCRIPT_TYPE + "'], link[rel='serialization']";
Array.prototype.forEach.call(
doc.querySelectorAll(selector),
function (element) {
element.parentNode.removeChild(element);
}
);
}
},
_addObjects: {
value: function (doc, objectsString) {
if (objectsString) {
var script = doc.createElement("script");
script.setAttribute("type", this._SERIALIZATION_SCRIPT_TYPE);
script.textContent = JSON.stringify(JSON.parse(objectsString), null, 4);
doc.head.appendChild(script);
}
}
},
_templateFromElementContentsCache: {
value: null
},
clearTemplateFromElementContentsCache: {
value: function () {
this._templateFromElementContentsCache = null;
}
},
createTemplateFromElementContents: {
value: function (elementId) {
var element,
template,
range,
cache = this._templateFromElementContentsCache;
if (!cache) {
cache = Object.create(null);
this._templateFromElementContentsCache = cache;
}
if (elementId in cache) {
// We always return an extension of the cached object, this
// is because the template can be assigned with instances.
// An alternate idea would be to clone it but it's much more
// expensive.
return Object.create(cache[elementId]);
}
element = this.getElementById(elementId);
// Clone the element contents
range = this.document.createRange();
range.selectNodeContents(element);
// Create the new template with the extracted serialization and
// markup.
template = this.createTemplateFromRange(range);
cache[elementId] = template;
// We always return an extension of the cached object, this
// is because the template is mutable.
// An alternate idea would be to clone it but it's much more
// expensive.
return Object.create(template);
}
},
createTemplateFromElement: {
value: function (elementId) {
var element,
range;
element = this.getElementById(elementId);
// Clone the element contents
range = this.document.createRange();
range.selectNode(element);
return this.createTemplateFromRange(range);
}
},
createTemplateFromRange: {
value: function (range) {
var fragment,
elementIds,
labels,
template,
serialization = new Serialization(),
extractedSerialization;
fragment = range.cloneContents();
// Find all elements of interest to the serialization.
elementIds = this._getChildrenElementIds(fragment);
// Create a new serialization with the components found in the
// element.
serialization.initWithString(this.objectsString);
labels = serialization.getSerializationLabelsWithElements(
elementIds);
extractedSerialization = serialization.extractSerialization(
labels, ["owner"]);
// Create the new template with the extracted serialization and
// markup.
template = new Template();
template.initWithObjectsAndDocumentFragment(
null, fragment, this._require);
template.objectsString = extractedSerialization
.getSerializationString();
template._resources = this.getResources();
return template;
}
},
// TODO: should this be on Serialization?
_createSerializationWithElementIds: {
value: function (elementIds) {
var serialization = new Serialization(),
labels,
extractedSerialization;
serialization.initWithString(this.objectsString);
labels = serialization.getSerializationLabelsWithElements(
elementIds);
extractedSerialization = serialization.extractSerialization(
labels, ["owner"]);
return extractedSerialization;
}
},
/**
* @param {TemplateArgumentProvider} templateArgumentProvider An object that
* implements the interface needed to provide the arguments to the
* parameters.
* @returns {Object} A dictionary with four properties representing the
* objects and elements that were imported into the template:
* - labels: the labels of the objects added from template
* argument.
* - labelsCollisions: a dictionary of label collisions in the form
* of {oldLabel: newLabel}.
* - elementIds: the element ids of the markup imported from
* template argument.
* - elementIdsCollisions: a dictionary of element id collisions in
* the form of {oldElementId: newElementId}
*
*/
expandParameters: {
value: function (templateArgumentProvider) {
var parameterElements,
argumentsElementIds = [],
collisionTable,
argumentElementsCollisionTable = {},
objectsCollisionTable,
parameterElement,
argumentElement,
serialization = this.getSerialization(),
argumentsSerialization,
willMergeObjectWithLabel,
result = {};
parameterElements = this.getParameters();
// Expand elements.
for (var parameterName in parameterElements) {
if (parameterElements.hasOwnProperty(parameterName)) {
parameterElement = parameterElements[parameterName];
argumentElement = templateArgumentProvider.getTemplateArgumentElement(
parameterName);
// Store all element ids of the argument, we need to create
// a serialization with the components that point to them.
argumentsElementIds.push.apply(argumentsElementIds,
this._getElementIds(argumentElement)
);
// Replace the parameter with the argument and save the
// element ids collision table because we need to correct the
// serialization that is created from the stored element ids.
collisionTable = this.replaceNode(argumentElement, parameterElement);
if (collisionTable) {
/* jshint forin: true */
for (var key in collisionTable) {
/* jshint forin: false */
argumentElementsCollisionTable[key] = collisionTable[key];
}
}
}
}
result.elementIds = argumentsElementIds;
result.elementIdsCollisions = argumentElementsCollisionTable;
// Expand objects.
argumentsSerialization = templateArgumentProvider
.getTemplateArgumentSerialization(argumentsElementIds);
argumentsSerialization.renameElementReferences(
argumentElementsCollisionTable);
// When merging the serializations we need to resolve any template
// property alias that comes from the arguments, for instance, the
// argument could be referring to @table:cell in its scope when in
// this scope (the serialization1) it is aliased to
// @repetition:iteration. To do this we ask the argument provider
// to resolve the template property for us.
// This approach works because the arguments serialization is
// created assuming that template properties are just like any other
// label and are considered external objects.
willMergeObjectWithLabel = function (label) {
if (label.indexOf(":") > 0) {
return templateArgumentProvider
.resolveTemplateArgumentTemplateProperty(label);
}
};
objectsCollisionTable = serialization.mergeSerialization(
argumentsSerialization, {
willMergeObjectWithLabel: willMergeObjectWithLabel
});
this.objectsString = serialization.getSerializationString();
result.labels = argumentsSerialization.getSerializationLabels();
result.labelsCollisions = objectsCollisionTable;
return result;
}
},
/**
* Takes a foreign node and generate new ids for all element ids that
* already exist in the current template.
*/
_resolveElementIdCollisions: {
value: function (node, labeler) {
var collisionTable,
nodeElements,
elementId,
elementIds,
element,
newId;
labeler = labeler || new MontageLabeler();
// Set up the labeler with the current element ids.
elementIds = this.getElementIds();
for (var i = 0; (elementId = elementIds[i]); i++) {
labeler.addLabel(elementId);
}
// Resolve element ids collisions.
nodeElements = this._getElements(node);
for (elementId in nodeElements) {
if (this.getElementById(elementId)) {
element = nodeElements[elementId];
newId = labeler.generateLabel(labeler.getLabelBaseName(elementId));
this.setElementId(element, newId);
if (!collisionTable) {
collisionTable = Object.create(null);
}
collisionTable[elementId] = newId;
}
}
return collisionTable;
}
},
replaceNode: {
value: function (newNode, oldNode, labeler) {
var collisionTable;
collisionTable = this._resolveElementIdCollisions(newNode, labeler);
this.normalizeRelativeUrls(newNode, this.getBaseUrl());
oldNode.parentNode.replaceChild(newNode, oldNode);
return collisionTable;
}
},
insertNodeBefore: {
value: function (node, reference, labeler) {
var collisionTable;
collisionTable = this._resolveElementIdCollisions(node, labeler);
this.normalizeRelativeUrls(node, this.getBaseUrl());
reference.parentNode.insertBefore(node, reference);
return collisionTable;
}
},
appendNode: {
value: function (node, parentNode, labeler) {
var collisionTable;
collisionTable = this._resolveElementIdCollisions(node, labeler);
this.normalizeRelativeUrls(node, this.getBaseUrl());
parentNode.appendChild(node);
return collisionTable;
}
},
getElementId: {
value: function (element) {
if (element.getAttribute) {
return element.getAttribute(this._ELEMENT_ID_ATTRIBUTE);
}
}
},
setElementId: {
value: function (element, elementId) {
element.setAttribute(this._ELEMENT_ID_ATTRIBUTE, elementId);
}
},
getElementIds: {
value: function () {
return this._getElementIds(this.document.body);
}
},
_getElements: {
value: function (rootNode) {
var selector = "*[" + this._ELEMENT_ID_ATTRIBUTE + "]",
elements,
result = {},
elementId;
elements = rootNode.querySelectorAll(selector);
for (var i = 0, element; (element = elements[i]); i++) {
elementId = this.getElementId(element);
result[elementId] = element;
}
elementId = this.getElementId(rootNode);
if (elementId) {
result[elementId] = rootNode;
}
return result;
}
},
_getChildrenElementIds: {
value: function (rootNode) {
// XPath might do a better job here...should test.
var selector = "*[" + this._ELEMENT_ID_ATTRIBUTE + "]",
elements,
elementIds = [];
elements = rootNode.querySelectorAll(selector);
for (var i = 0, element; (element = elements[i]); i++) {
elementIds.push(this.getElementId(element));
}
return elementIds;
}
},
_getElementIds: {
value: function (rootNode) {
var elementIds = this._getChildrenElementIds(rootNode),
elementId;
elementId = this.getElementId(rootNode);
if (elementId) {
elementIds.push(elementId);
}
return elementIds;
}
},
getElementById: {
value: function (elementId) {
var selector = "*[" + this._ELEMENT_ID_ATTRIBUTE + "='" + elementId + "']";
return this.document.querySelector(selector);
}
},
html: {
get: function () {
var _document = this.document;
this._removeObjects(_document);
this._addObjects(_document, this.objectsString);
return this._getDoctypeString(_document.doctype) + "\n" +
_document.documentElement.outerHTML;
}
},
_getDoctypeString: {
value: function (doctype) {
return "<!DOCTYPE " +
doctype.name +
(doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '') +
(!doctype.publicId && doctype.systemId ? ' SYSTEM' : '') +
(doctype.systemId ? ' "' + doctype.systemId + '"' : '') +
'>';
}
},
normalizeRelativeUrls: {
value: function (parentNode, baseUrl) {
// Resolve component's images relative URLs if we have a valid baseUrl
if (typeof baseUrl === "string" && baseUrl !== "" && baseUrl !== 'about:blank') {
// We are only looking for DOM and SVG image elements
var XLINK_NS = 'http://www.w3.org/1999/xlink', // Namespace for SVG's xlink
absoluteUrlRegExp = /^[\w\-]+:|^\//, // Check for "<protocol>:", "/" and "//",
nodes = Template._NORMALIZED_TAG_NAMES.indexOf(parentNode.tagName) !== -1 ?
[parentNode] : parentNode.querySelectorAll(Template._NORMALIZED_TAG_NAMES_SELECTOR);
for (var i = 0, ii = nodes.length; i < ii; i++) {
var node = nodes[i],
url;
if (node.tagName === 'image') {
// SVG image
url = node.getAttributeNS(XLINK_NS, 'href');
if (!absoluteUrlRegExp.test(url)) {
node.setAttributeNS(XLINK_NS, 'href', URL.resolve(baseUrl, url));
}
} else {
// DOM image
if (node.hasAttribute("src")) {
url = node.getAttribute('src');
if (url !== "" && !absoluteUrlRegExp.test(url)) {
node.setAttribute('src', URL.resolve(baseUrl, url));
}
} else if (node.hasAttribute("href")) {
// Stylesheets
url = node.getAttribute('href');
if (url !== "" && !absoluteUrlRegExp.test(url)) {
node.setAttribute('href', URL.resolve(baseUrl, url));
}
}
}
}
}
}
},
replaceContentsWithTemplate: {
value: function (template) {
this._require = template._require;
this._baseUrl = template._baseUrl;
this._document = template._document;
this.objectsString = template.objectsString;
this._instances = template._instances;
this._templateFromElementContentsCache = template._templateFromElementContentsCache;
this._metadata = template._metadata;
}
},
/**
* Refresh the contents of the template when its dirty.
*/
refresh: {
value: function () {
if (this.isDirty) {
if (this.refresher &&
typeof this.refresher.refreshTemplate === "function") {
this.refresher.refreshTemplate(this);
this.isDirty = false;
} else {
console.warn("Not able to refresh without a refresher.refreshTemplate.");
}
}
}
}
}, {
_templateCache: {
value: {
moduleId: Object.create(null)
}
},
_getTemplateCacheKey: {
value: function (moduleId, _require) {
// Transforms relative module ids into absolute module ids
moduleId = _require.resolve(moduleId);
return _require.location + "#" + moduleId;
}
},
getTemplateWithModuleId: {
value: function (moduleId, _require) {
var cacheKey,
template;
cacheKey = this._getTemplateCacheKey(moduleId, _require);
template = this._templateCache.moduleId[cacheKey];
if (!template) {
template = new Template()
.initWithModuleId(moduleId, _require);
this._templateCache.moduleId[cacheKey] = template;
}
return template;
}
},
_NORMALIZED_TAG_NAMES: {
value: ["IMG", "image", "IFRAME", "link","script"]
},
__NORMALIZED_TAG_NAMES_SELECTOR: {
value: null
},
_NORMALIZED_TAG_NAMES_SELECTOR: {
get: function () {
if (!this.__NORMALIZED_TAG_NAMES_SELECTOR) {
this.__NORMALIZED_TAG_NAMES_SELECTOR = this._NORMALIZED_TAG_NAMES.join(",");
}
return this.__NORMALIZED_TAG_NAMES_SELECTOR;
}
}
});
/**
* @class TemplateResources
* @extends Montage
*/
var TemplateResources = Montage.specialize( /** @lends TemplateResources# */ {
_resources: {value: null},
_resourcesLoaded: {value: false},
template: {value: null},
rootUrl: {value: ""},
constructor: {
value: function TemplateResources() {
this._resources = Object.create(null);
}
},
initWithTemplate: {
value: function (template) {
this.template = template;
}
},
hasResources: {
value: function () {
return this.getStyles().length > 0 || this.getScripts().length > 0;
}
},
resourcesLoaded: {
value: function () {
return this._resourcesLoaded;
}
},
loadResources: {
value: function (targetDocument) {
this._resourcesLoaded = true;
return Promise.all([
this.loadScripts(targetDocument),
this.loadStyles(targetDocument)
]);
}
},
getScripts: {
value: function () {
var scripts = this._resources.scripts,
script,
template,
templateScripts;
if (!scripts) {
template = this.template;
scripts = this._resources.scripts = [];
templateScripts = template.document.querySelectorAll("script");
for (var i = 0, ii = templateScripts.length; i < ii; i++) {
script = templateScripts[i];
if (script.type !== this.template._SERIALIZATION_SCRIPT_TYPE) {
scripts.push(script);
}
}
}
return scripts;
}
},
loadScripts: {
value: function (targetDocument) {
var scripts = this.getScripts(),
ii = scripts.length;
if (ii) {
var promises = [];
for (var i = 0; i < ii; i++) {
promises.push(
this.loadScript(scripts[i], targetDocument)
);
}
return Promise.all(promises);
}
return Promise.resolve();
}
},
loadScript: {
value: function (script, targetDocument) {
var documentResources,
newScript;
documentResources = DocumentResources.getInstanceForDocument(targetDocument);
// Firefox isn't able to load a script that we reuse, we need to
// create a new one :(.
//newScript = targetDocument.importNode(script);
newScript = this._cloneScriptElement(script, targetDocument);
return documentResources.addScript(newScript);
}
},
_cloneScriptElement: {
value: function (scriptTemplate, _document) {
var script = _document.createElement("script"),
attributes = scriptTemplate.attributes,
attribute;
for (var i = 0, ii = attributes.length; i < ii; i++) {
attribute = attributes[i];
script.setAttribute(attribute.name, attribute.value);
}
script.textContent = scriptTemplate.textContent;
return script;
}
},
getStyles: {
value: function () {
var styles = this._resources.styles,
template,
templateStyles,
styleSelector;
if (!styles) {
styleSelector = 'link[rel="stylesheet"], style';
template = this.template;
templateStyles = template.document.querySelectorAll(styleSelector);
styles = Array.prototype.slice.call(templateStyles, 0);
this._resources.styles = styles;
}
return styles;
}
},
loadStyles: {
value: function (targetDocument) {
var promises = [],
styles;
styles = this.getStyles();
for (var i = 0, ii = styles.length; i < ii; i++) {
promises.push(
this.loadStyle(styles[i], targetDocument)
);
}
return Promise.all(promises);
}
},
loadStyle: {
value: function (element, targetDocument) {
var url,
documentResources;
url = element.getAttribute("href");
if (url) {
documentResources = DocumentResources.getInstanceForDocument(targetDocument);
return documentResources.preloadResource(url);
} else {
return Promise.resolve();
}
}
},
createStylesForDocument: {
value: function (targetDocument) {
var styles = this.getStyles(),
newStyle,
stylesForDocument = [];
for (var i = 0, style; (style = styles[i]); i++) {
newStyle = targetDocument.importNode(style, true);
stylesForDocument.push(newStyle);
}
return stylesForDocument;
}
}
});
// Used to create a DocumentPart from a document without a Template
function instantiateDocument(_document, _require, instances) {
var template = new Template(),
html = _document.documentElement.outerHTML,
part = new DocumentPart(),
clonedDocument,
templateObjects,
rootElement = _document.documentElement;
// Setup a template just like we'd do for a document in a template
clonedDocument = template.createHtmlDocumentWithHtml(html, _document.location.href);
return template.initWithDocument(clonedDocument, _require)
.then(function () {
template.setBaseUrl(_document.location.href);
// Instantiate it using the document given since we don't want to clone
// the document markup
templateObjects = template._createTemplateObjects(instances);
part.initWithTemplateAndFragment(template);
return template._instantiateObjects(templateObjects, rootElement)
.then(function (objects) {
part.objects = objects;
template._invokeDelegates(part);
return part;
});
});
}
var TemplateArgumentProvider = Montage.specialize({
/**
* This function asks the provider to return the element that corresponds
* to the argument with the same name. This element will be used to replace
* the corresponding element with data-param of the template being expanded.
* @param argumentName
* @private
*/
getTemplateArgumentElement: {
value: Function.noop
},
/**
* This function asks the provider to return the serialization components
* that refer to the given element ids.
* The serialization returned will be merged with the serialization of the
* template being expanded.
* @param elementIds
* @private
*/
getTemplateArgumentSerialization: {
value: Function.noop
},
/**
* This function asks the provider to resolve a template property that was
* found in the argument serialization. The template property could be an
* alias that only the provider knows how to resolve because they have
* access to the template where the argument comes from and where the
* aliases are defined in the serialization block (e.g: ":cell": {alias:
* "@repetition:iteration"}).
* @param templatePropertyLabel
* @private
*/
resolveTemplateArgumentTemplateProperty: {
value: Function.noop
}
});
exports.Template = Template;
exports.TemplateArgumentProvider = TemplateArgumentProvider;
exports.TemplateResources = TemplateResources;
exports.instantiateDocument = instantiateDocument;