Bug 901521 - [app manager] implement a template mechanism based on stores. r=mratcliffe

This commit is contained in:
Paul Rouget 2013-08-21 08:56:40 +02:00
parent 325b07e4fd
commit aa4f0af233
8 changed files with 607 additions and 1 deletions

View File

@ -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

View File

@ -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)
*
* <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._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);
}
},
}

View File

@ -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']

View File

@ -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

View File

@ -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/.

View File

@ -0,0 +1,239 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<div id="root">
<span template='{"type":"textContent","path":"title"}'></span>
<span template='{"type":"attribute","name":"title","path":"title"}'></span>
<span template='{"type":"localizedContent","paths":["foo2.foo_l10n","foo2.bar_l10n"],"property":"foo2"}'></span>
<div template-loop='{"arrayPath":"foo1.bar1","childSelector":"#template"}'></div>
</div>
<div id="ref0">
<span template='{"type":"textContent","path":"title"}'>ttt</span>
<span title="ttt" template='{"type":"attribute","name":"title","path":"title"}'></span>
<span template='{"type":"localizedContent","paths":["foo2.foo_l10n","foo2.bar_l10n"],"property":"foo2"}'>foo2:foo_l10n/bar_l10n</span>
<div template-loop='{"arrayPath":"foo1.bar1","childSelector":"#template"}'>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.0."}'>xx0</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.0."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.0."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.1."}'>xx1</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.1."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.1."}'>b</span>
</div>
</div>
</div>
<div id="ref1">
<span template='{"type":"textContent","path":"title"}'>xxx</span>
<span title="xxx" template='{"type":"attribute","name":"title","path":"title"}'></span>
<span template='{"type":"localizedContent","paths":["foo2.foo_l10n","foo2.bar_l10n"],"property":"foo2"}'>foo2:foo2_l10n/bar_l10n</span>
<div template-loop='{"arrayPath":"foo1.bar1","childSelector":"#template"}'>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.0."}'>xx0</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.0."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.0."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.1."}'>xx1</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.1."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.1."}'>b</span>
</div>
</div>
</div>
<div id="ref2">
<span template='{"type":"textContent","path":"title"}'>xxx</span>
<span title="xxx" template='{"type":"attribute","name":"title","path":"title"}'></span>
<span template='{"type":"localizedContent","paths":["foo2.foo_l10n","foo2.bar_l10n"],"property":"foo2"}'>foo2:yyy/zzz</span>
<div template-loop='{"arrayPath":"foo1.bar1","childSelector":"#template"}'>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.0."}'>xx0</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.0."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.0."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.1."}'>xx1</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.1."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.1."}'>b</span>
</div>
</div>
</div>
<div id="ref3">
<span template='{"type":"textContent","path":"title"}'>xxx</span>
<span title="xxx" template='{"type":"attribute","name":"title","path":"title"}'></span>
<span template='{"type":"localizedContent","paths":["foo2.foo_l10n","foo2.bar_l10n"],"property":"foo2"}'>foo2:yyy/zzz</span>
<div template-loop='{"arrayPath":"foo1.bar1","childSelector":"#template"}'>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.0."}'>xx0</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.0."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.0."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.1."}'>xx1</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.1."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.1."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.2."}'>xx2</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.2."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.2."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.3."}'>xx3</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.3."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.3."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.4."}'>xx4</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.4."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.4."}'>b</span>
</div>
</div>
</div>
<div id="ref4">
<span template='{"type":"textContent","path":"title"}'>xxx</span>
<span title="xxx" template='{"type":"attribute","name":"title","path":"title"}'></span>
<span template='{"type":"localizedContent","paths":["foo2.foo_l10n","foo2.bar_l10n"],"property":"foo2"}'>foo2:yyy/zzz</span>
<div template-loop='{"arrayPath":"foo1.bar1","childSelector":"#template"}'>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.0."}'>xx0</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.0."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.0."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.1."}'>xx1</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.1."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.1."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.2."}'>xx2</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.2."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.2."}'>b</span>
</div>
<div>
<span template='{"type":"textContent","path":"idx","rootPath":"foo1.bar1.3."}'>xx3</span>
<span template='{"type":"textContent","path":"a","rootPath":"foo1.bar1.3."}'>a</span>
<span template='{"type":"textContent","path":"b","rootPath":"foo1.bar1.3."}'>b</span>
</div>
</div>
</div>
<template id="template">
<div>
<span template='{"type":"textContent","path":"idx"}'></span>
<span template='{"type":"textContent","path":"a"}'></span>
<span template='{"type":"textContent","path":"b"}'></span>
</div>
</template>
<script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/app-manager/template.js"></script>
<script type="application/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
const Cu = Components.utils;
Cu.import("resource:///modules/devtools/gDevTools.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const ObservableObject = require("devtools/shared/observable-object");
let data = {
title: "ttt",
foo1: {
bar1: [
{idx: "xx0", a: "a", b: "b"},
{idx: "xx1", a: "a", b: "b"},
],
},
foo2: {
foo_l10n: "foo_l10n",
bar_l10n: "bar_l10n"
},
};
let store = new ObservableObject(data);
let changes = [
{
exec: function() {},
reference: document.querySelector("#ref0")
},
{
exec: function() {
store.object.title = "xxx";
store.object.foo2.foo_l10n = "foo2_l10n";
},
reference: document.querySelector("#ref1")
},
{
exec: function() {
store.object.foo2 = {
foo_l10n: "yyy",
bar_l10n: "zzz",
}
},
reference: document.querySelector("#ref2")
},
{
exec: function() {
let items = [];
for (let i = 2; i < 5; i++) {
items.push({idx: "xx" + i, a: "a", b: "b"});
}
store.object.foo1.bar1 = store.object.foo1.bar1.concat(items);
},
reference: document.querySelector("#ref3")
},
{
exec: function() {
store.object.foo1.bar1.pop();
},
reference: document.querySelector("#ref4")
},
];
function compare(node1, node2) {
let text1 = node1.innerHTML;
let text2 = node2.innerHTML;
text1 = text1.replace(/\n/g,"");
text2 = text2.replace(/\n/g,"");
text1 = text1.replace(/\s+/g,"");
text2 = text2.replace(/\s+/g,"");
return text1 == text2;
}
let root = document.querySelector("#root");
let t = new Template(root, store, (prop, args) => {
return prop + ":" + args.join("/");
});
t.start();
for (let i = 0; i < changes.length; i++) {
let change = changes[i];
change.exec();
ok(compare(change.reference, root), "Content " + i + " looks good.");
}
SimpleTest.finish();
</script>
</html>

View File

@ -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)

View File

@ -22,5 +22,5 @@ DIRS += [
'framework',
'profiler',
'fontinspector',
'app-manager',
]