зеркало из https://github.com/mozilla/gecko-dev.git
392 строки
11 KiB
JavaScript
392 строки
11 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* Template mechanism based on Object Emitters.
|
|
*
|
|
* The data used to expand the templates comes from
|
|
* a ObjectEmitter object. The templates are automatically
|
|
* updated as the ObjectEmitter is updated (via the "set"
|
|
* event). See documentation in observable-object.js.
|
|
*
|
|
* Templates are used this way:
|
|
*
|
|
* (See examples in browser/devtools/app-manager/content/*.xhtml)
|
|
*
|
|
* <div template="{JSON Object}">
|
|
*
|
|
* {
|
|
* type: "attribute"
|
|
* name: name of the attribute
|
|
* path: location of the attribute value in the ObjectEmitter
|
|
* }
|
|
*
|
|
* {
|
|
* type: "textContent"
|
|
* path: location of the textContent value in the ObjectEmitter
|
|
* }
|
|
*
|
|
* {
|
|
* type: "localizedContent"
|
|
* paths: array of locations of the value of the arguments of the property
|
|
* property: l10n property
|
|
* }
|
|
*
|
|
* <div template-loop="{JSON Object}">
|
|
*
|
|
* {
|
|
* arrayPath: path of the array in the ObjectEmitter to loop from
|
|
* childSelector: selector of the element to duplicate in the loop
|
|
* }
|
|
*
|
|
*/
|
|
|
|
const NOT_FOUND_STRING = "n/a";
|
|
|
|
/**
|
|
* let t = new Template(root, store, l10nResolver);
|
|
* t.start();
|
|
*
|
|
* @param DOMNode root.
|
|
* Node from where templates are expanded.
|
|
* @param ObjectEmitter store.
|
|
* ObjectEmitter object.
|
|
* @param function (property, args). l10nResolver
|
|
* A function that returns localized content.
|
|
*/
|
|
function Template(root, store, l10nResolver) {
|
|
this._store = store;
|
|
this._l10n = l10nResolver;
|
|
|
|
// Listeners are stored in Maps.
|
|
// path => Set(node1, node2, ..., nodeN)
|
|
// For example: "foo.bar.4.name" => Set(div1,div2)
|
|
|
|
this._nodeListeners = new Map();
|
|
this._loopListeners = new Map();
|
|
this._forListeners = new Map();
|
|
this._root = root;
|
|
this._doc = this._root.ownerDocument;
|
|
|
|
this._storeChanged = this._storeChanged.bind(this);
|
|
this._store.on("set", this._storeChanged);
|
|
}
|
|
|
|
Template.prototype = {
|
|
start: function() {
|
|
this._processTree(this._root);
|
|
},
|
|
|
|
destroy: function() {
|
|
this._store.off("set", this._storeChanged);
|
|
this._root = null;
|
|
this._doc = null;
|
|
},
|
|
|
|
_resolvePath: function(path, defaultValue=null) {
|
|
|
|
// From the store, get the value of an object located
|
|
// at @path.
|
|
//
|
|
// For example, if the store is designed as:
|
|
//
|
|
// {
|
|
// foo: {
|
|
// bar: [
|
|
// {},
|
|
// {},
|
|
// {a: 2}
|
|
// }
|
|
// }
|
|
//
|
|
// _resolvePath("foo.bar.2.a") will return "2".
|
|
//
|
|
// Array indexes are not surrounded by brackets.
|
|
|
|
let chunks = path.split(".");
|
|
let obj = this._store.object;
|
|
for (let word of chunks) {
|
|
if ((typeof obj) == "object" &&
|
|
(word in obj)) {
|
|
obj = obj[word];
|
|
} else {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
_storeChanged: function(event, path, value) {
|
|
|
|
// The store has changed (a "set" event has been emitted).
|
|
// We need to invalidate and rebuild the affected elements.
|
|
|
|
let strpath = path.join(".");
|
|
this._invalidate(strpath);
|
|
|
|
for (let [registeredPath, set] of this._nodeListeners) {
|
|
if (strpath != registeredPath &&
|
|
registeredPath.indexOf(strpath) > -1) {
|
|
this._invalidate(registeredPath);
|
|
}
|
|
}
|
|
},
|
|
|
|
_invalidate: function(path) {
|
|
// Loops:
|
|
let set = this._loopListeners.get(path);
|
|
if (set) {
|
|
for (let elt of set) {
|
|
this._processLoop(elt);
|
|
}
|
|
}
|
|
|
|
// For:
|
|
set = this._forListeners.get(path);
|
|
if (set) {
|
|
for (let elt of set) {
|
|
this._processFor(elt);
|
|
}
|
|
}
|
|
|
|
// Nodes:
|
|
set = this._nodeListeners.get(path);
|
|
if (set) {
|
|
for (let elt of set) {
|
|
this._processNode(elt);
|
|
}
|
|
}
|
|
},
|
|
|
|
_registerNode: function(path, element) {
|
|
|
|
// We map a node to a path.
|
|
// If the value behind this path is updated,
|
|
// we get notified from the ObjectEmitter,
|
|
// and then we know which objects to update.
|
|
|
|
if (!this._nodeListeners.has(path)) {
|
|
this._nodeListeners.set(path, new Set());
|
|
}
|
|
let set = this._nodeListeners.get(path);
|
|
set.add(element);
|
|
},
|
|
|
|
_unregisterNodes: function(nodes) {
|
|
for (let [registeredPath, set] of this._nodeListeners) {
|
|
for (let e of nodes) {
|
|
set.delete(e);
|
|
}
|
|
if (set.size == 0) {
|
|
this._nodeListeners.delete(registeredPath);
|
|
}
|
|
}
|
|
},
|
|
|
|
_registerLoop: function(path, element) {
|
|
if (!this._loopListeners.has(path)) {
|
|
this._loopListeners.set(path, new Set());
|
|
}
|
|
let set = this._loopListeners.get(path);
|
|
set.add(element);
|
|
},
|
|
|
|
_registerFor: function(path, element) {
|
|
if (!this._forListeners.has(path)) {
|
|
this._forListeners.set(path, new Set());
|
|
}
|
|
let set = this._forListeners.get(path);
|
|
set.add(element);
|
|
},
|
|
|
|
_processNode: function(element, rootPath="") {
|
|
// The actual magic.
|
|
// The element has a template attribute.
|
|
// The value is supposed to be a JSON string.
|
|
// rootPath is the prefex to the path used by
|
|
// these elements (if children of template-loop);
|
|
|
|
let e = element;
|
|
let str = e.getAttribute("template");
|
|
|
|
if (rootPath) {
|
|
// We will prefix paths with this rootPath.
|
|
// It needs to end with a dot.
|
|
rootPath = rootPath + ".";
|
|
}
|
|
|
|
try {
|
|
let json = JSON.parse(str);
|
|
// Sanity check
|
|
if (!("type" in json)) {
|
|
throw new Error("missing property");
|
|
}
|
|
if (json.rootPath) {
|
|
// If node has been generated through a loop, we stored
|
|
// previously its rootPath.
|
|
rootPath = json.rootPath;
|
|
}
|
|
|
|
// paths is an array that will store all the paths we needed
|
|
// to expand the node. We will then, via _registerNode, link
|
|
// this element to these paths.
|
|
|
|
let paths = [];
|
|
|
|
switch (json.type) {
|
|
case "attribute": {
|
|
if (!("name" in json) ||
|
|
!("path" in json)) {
|
|
throw new Error("missing property");
|
|
}
|
|
e.setAttribute(json.name, this._resolvePath(rootPath + json.path, NOT_FOUND_STRING));
|
|
paths.push(rootPath + json.path);
|
|
break;
|
|
}
|
|
case "textContent": {
|
|
if (!("path" in json)) {
|
|
throw new Error("missing property");
|
|
}
|
|
e.textContent = this._resolvePath(rootPath + json.path, NOT_FOUND_STRING);
|
|
paths.push(rootPath + json.path);
|
|
break;
|
|
}
|
|
case "localizedContent": {
|
|
if (!("property" in json) ||
|
|
!("paths" in json)) {
|
|
throw new Error("missing property");
|
|
}
|
|
let params = json.paths.map((p) => {
|
|
paths.push(rootPath + p);
|
|
let str = this._resolvePath(rootPath + p, NOT_FOUND_STRING);
|
|
return str;
|
|
});
|
|
e.textContent = this._l10n(json.property, params);
|
|
break;
|
|
}
|
|
}
|
|
if (rootPath) {
|
|
// We save the rootPath if any.
|
|
json.rootPath = rootPath;
|
|
e.setAttribute("template", JSON.stringify(json));
|
|
}
|
|
if (paths.length > 0) {
|
|
for (let path of paths) {
|
|
this._registerNode(path, e);
|
|
}
|
|
}
|
|
} catch(exception) {
|
|
console.error("Invalid template: " + e.outerHTML + " (" + exception + ")");
|
|
}
|
|
},
|
|
|
|
_processLoop: function(element, rootPath="") {
|
|
// The element has a template-loop attribute.
|
|
// The related path must be an array. We go
|
|
// through the array, and build one child per
|
|
// item. The template for this child is pointed
|
|
// by the childSelector property.
|
|
let e = element;
|
|
try {
|
|
let template, count;
|
|
let str = e.getAttribute("template-loop");
|
|
let json = JSON.parse(str);
|
|
if (!("arrayPath" in json) ||
|
|
!("childSelector" in json)) {
|
|
throw new Error("missing property");
|
|
}
|
|
if (rootPath) {
|
|
json.arrayPath = rootPath + "." + json.arrayPath;
|
|
}
|
|
let templateParent = this._doc.querySelector(json.childSelector);
|
|
if (!templateParent) {
|
|
throw new Error("can't find child");
|
|
}
|
|
template = this._doc.createElement("div");
|
|
template.innerHTML = templateParent.innerHTML;
|
|
template = template.firstElementChild;
|
|
let array = this._resolvePath(json.arrayPath, []);
|
|
if (!Array.isArray(array)) {
|
|
console.error("referenced array is not an array");
|
|
}
|
|
count = array.length;
|
|
|
|
let fragment = this._doc.createDocumentFragment();
|
|
for (let i = 0; i < count; i++) {
|
|
let node = template.cloneNode(true);
|
|
this._processTree(node, json.arrayPath + "." + i);
|
|
fragment.appendChild(node);
|
|
}
|
|
this._registerLoop(json.arrayPath, e);
|
|
this._registerLoop(json.arrayPath + ".length", e);
|
|
this._unregisterNodes(e.querySelectorAll("[template]"));
|
|
e.innerHTML = "";
|
|
e.appendChild(fragment);
|
|
} catch(exception) {
|
|
console.error("Invalid template: " + e.outerHTML + " (" + exception + ")");
|
|
}
|
|
},
|
|
|
|
_processFor: function(element, rootPath="") {
|
|
let e = element;
|
|
try {
|
|
let template;
|
|
let str = e.getAttribute("template-for");
|
|
let json = JSON.parse(str);
|
|
if (!("path" in json) ||
|
|
!("childSelector" in json)) {
|
|
throw new Error("missing property");
|
|
}
|
|
|
|
if (rootPath) {
|
|
json.path = rootPath + "." + json.path;
|
|
}
|
|
|
|
if (!json.path) {
|
|
// Nothing to show.
|
|
this._unregisterNodes(e.querySelectorAll("[template]"));
|
|
e.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
let templateParent = this._doc.querySelector(json.childSelector);
|
|
if (!templateParent) {
|
|
throw new Error("can't find child");
|
|
}
|
|
let content = this._doc.createElement("div");
|
|
content.innerHTML = templateParent.innerHTML;
|
|
content = content.firstElementChild;
|
|
|
|
this._processTree(content, json.path);
|
|
|
|
this._unregisterNodes(e.querySelectorAll("[template]"));
|
|
this._registerFor(json.path, e);
|
|
|
|
e.innerHTML = "";
|
|
e.appendChild(content);
|
|
|
|
} catch(exception) {
|
|
console.error("Invalid template: " + e.outerHTML + " (" + exception + ")");
|
|
}
|
|
},
|
|
|
|
_processTree: function(parent, rootPath="") {
|
|
let loops = parent.querySelectorAll(":not(template) [template-loop]");
|
|
let fors = parent.querySelectorAll(":not(template) [template-for]");
|
|
let nodes = parent.querySelectorAll(":not(template) [template]");
|
|
for (let e of loops) {
|
|
this._processLoop(e, rootPath);
|
|
}
|
|
for (let e of fors) {
|
|
this._processFor(e, rootPath);
|
|
}
|
|
for (let e of nodes) {
|
|
this._processNode(e, rootPath);
|
|
}
|
|
if (parent.hasAttribute("template")) {
|
|
this._processNode(parent, rootPath);
|
|
}
|
|
},
|
|
}
|