diff --git a/browser/devtools/app-manager/Makefile.in b/browser/devtools/app-manager/Makefile.in new file mode 100644 index 00000000000..de2b81dba52 --- /dev/null +++ b/browser/devtools/app-manager/Makefile.in @@ -0,0 +1,16 @@ +# +# 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/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/rules.mk + +libs:: + $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/app-manager diff --git a/browser/devtools/app-manager/content/template.js b/browser/devtools/app-manager/content/template.js new file mode 100644 index 00000000000..f43b85678c6 --- /dev/null +++ b/browser/devtools/app-manager/content/template.js @@ -0,0 +1,320 @@ +/* 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) + * + *
+ * + * { + * 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 + * } + * + *
+ * + * { + * 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._root = root; + this._doc = this._root.ownerDocument; + + this._store.on("set", (event,path,value) => this._storeChanged(path,value)); +} + +Template.prototype = { + start: function() { + this._processTree(this._root); + }, + + _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(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); + } + } + + // 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); + }, + + _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. + try { + let e = element; + 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 + ")"); + } + }, + + _processTree: function(parent, rootPath="") { + let loops = parent.querySelectorAll(":not(template) [template-loop]"); + let nodes = parent.querySelectorAll(":not(template) [template]"); + for (let e of loops) { + this._processLoop(e, rootPath); + } + for (let e of nodes) { + this._processNode(e, rootPath); + } + if (parent.hasAttribute("template")) { + this._processNode(parent, rootPath); + } + }, +} diff --git a/browser/devtools/app-manager/moz.build b/browser/devtools/app-manager/moz.build new file mode 100644 index 00000000000..86ec4674859 --- /dev/null +++ b/browser/devtools/app-manager/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +TEST_DIRS += ['test'] diff --git a/browser/devtools/app-manager/test/Makefile.in b/browser/devtools/app-manager/test/Makefile.in new file mode 100644 index 00000000000..6fdcef9f898 --- /dev/null +++ b/browser/devtools/app-manager/test/Makefile.in @@ -0,0 +1,17 @@ +# 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/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk + +MOCHITEST_CHROME_FILES = \ + test_template.html \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/devtools/app-manager/test/moz.build b/browser/devtools/app-manager/test/moz.build new file mode 100644 index 00000000000..895d11993cf --- /dev/null +++ b/browser/devtools/app-manager/test/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + diff --git a/browser/devtools/app-manager/test/test_template.html b/browser/devtools/app-manager/test/test_template.html new file mode 100644 index 00000000000..8f5aa351608 --- /dev/null +++ b/browser/devtools/app-manager/test/test_template.html @@ -0,0 +1,239 @@ + + + + + + + + + + + + +
+ + + +
+
+ +
+ ttt + + foo2:foo_l10n/bar_l10n +
+
+ xx0 + a + b +
+
+ xx1 + a + b +
+
+
+ + +
+ xxx + + foo2:foo2_l10n/bar_l10n +
+
+ xx0 + a + b +
+
+ xx1 + a + b +
+
+
+ +
+ xxx + + foo2:yyy/zzz +
+
+ xx0 + a + b +
+
+ xx1 + a + b +
+
+
+ +
+ xxx + + foo2:yyy/zzz +
+
+ xx0 + a + b +
+
+ xx1 + a + b +
+
+ xx2 + a + b +
+
+ xx3 + a + b +
+
+ xx4 + a + b +
+
+
+ +
+ xxx + + foo2:yyy/zzz +
+
+ xx0 + a + b +
+
+ xx1 + a + b +
+
+ xx2 + a + b +
+
+ xx3 + a + b +
+
+
+ + + + + + + + diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index e0ab285d1e3..65e3b436c64 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -65,3 +65,4 @@ browser.jar: content/browser/devtools/connect.xhtml (framework/connect/connect.xhtml) content/browser/devtools/connect.css (framework/connect/connect.css) content/browser/devtools/connect.js (framework/connect/connect.js) + content/browser/devtools/app-manager/template.js (app-manager/content/template.js) diff --git a/browser/devtools/moz.build b/browser/devtools/moz.build index 9cec3f5b0dd..9f66af5b660 100644 --- a/browser/devtools/moz.build +++ b/browser/devtools/moz.build @@ -22,5 +22,5 @@ DIRS += [ 'framework', 'profiler', 'fontinspector', - + 'app-manager', ]