browser/devtools/app-manager/content/template.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * Template mechanism based on Object Emitters.
michael@0 7 *
michael@0 8 * The data used to expand the templates comes from
michael@0 9 * a ObjectEmitter object. The templates are automatically
michael@0 10 * updated as the ObjectEmitter is updated (via the "set"
michael@0 11 * event). See documentation in observable-object.js.
michael@0 12 *
michael@0 13 * Templates are used this way:
michael@0 14 *
michael@0 15 * (See examples in browser/devtools/app-manager/content/*.xhtml)
michael@0 16 *
michael@0 17 * <div template="{JSON Object}">
michael@0 18 *
michael@0 19 * {
michael@0 20 * type: "attribute"
michael@0 21 * name: name of the attribute
michael@0 22 * path: location of the attribute value in the ObjectEmitter
michael@0 23 * }
michael@0 24 *
michael@0 25 * {
michael@0 26 * type: "textContent"
michael@0 27 * path: location of the textContent value in the ObjectEmitter
michael@0 28 * }
michael@0 29 *
michael@0 30 * {
michael@0 31 * type: "localizedContent"
michael@0 32 * paths: array of locations of the value of the arguments of the property
michael@0 33 * property: l10n property
michael@0 34 * }
michael@0 35 *
michael@0 36 * <div template-loop="{JSON Object}">
michael@0 37 *
michael@0 38 * {
michael@0 39 * arrayPath: path of the array in the ObjectEmitter to loop from
michael@0 40 * childSelector: selector of the element to duplicate in the loop
michael@0 41 * }
michael@0 42 *
michael@0 43 */
michael@0 44
michael@0 45 const NOT_FOUND_STRING = "n/a";
michael@0 46
michael@0 47 /**
michael@0 48 * let t = new Template(root, store, l10nResolver);
michael@0 49 * t.start();
michael@0 50 *
michael@0 51 * @param DOMNode root.
michael@0 52 * Node from where templates are expanded.
michael@0 53 * @param ObjectEmitter store.
michael@0 54 * ObjectEmitter object.
michael@0 55 * @param function (property, args). l10nResolver
michael@0 56 * A function that returns localized content.
michael@0 57 */
michael@0 58 function Template(root, store, l10nResolver) {
michael@0 59 this._store = store;
michael@0 60 this._rootResolver = new Resolver(this._store.object);
michael@0 61 this._l10n = l10nResolver;
michael@0 62
michael@0 63 // Listeners are stored in Maps.
michael@0 64 // path => Set(node1, node2, ..., nodeN)
michael@0 65 // For example: "foo.bar.4.name" => Set(div1,div2)
michael@0 66
michael@0 67 this._nodeListeners = new Map();
michael@0 68 this._loopListeners = new Map();
michael@0 69 this._forListeners = new Map();
michael@0 70 this._root = root;
michael@0 71 this._doc = this._root.ownerDocument;
michael@0 72
michael@0 73 this._queuedNodeRegistrations = [];
michael@0 74
michael@0 75 this._storeChanged = this._storeChanged.bind(this);
michael@0 76 this._store.on("set", this._storeChanged);
michael@0 77 }
michael@0 78
michael@0 79 Template.prototype = {
michael@0 80 start: function() {
michael@0 81 this._processTree(this._root);
michael@0 82 this._registerQueuedNodes();
michael@0 83 },
michael@0 84
michael@0 85 destroy: function() {
michael@0 86 this._store.off("set", this._storeChanged);
michael@0 87 this._root = null;
michael@0 88 this._doc = null;
michael@0 89 },
michael@0 90
michael@0 91 _storeChanged: function(event, path, value) {
michael@0 92
michael@0 93 // The store has changed (a "set" event has been emitted).
michael@0 94 // We need to invalidate and rebuild the affected elements.
michael@0 95
michael@0 96 let strpath = path.join(".");
michael@0 97 this._invalidate(strpath);
michael@0 98
michael@0 99 for (let [registeredPath, set] of this._nodeListeners) {
michael@0 100 if (strpath != registeredPath &&
michael@0 101 registeredPath.indexOf(strpath) > -1) {
michael@0 102 this._invalidate(registeredPath);
michael@0 103 }
michael@0 104 }
michael@0 105
michael@0 106 this._registerQueuedNodes();
michael@0 107 },
michael@0 108
michael@0 109 _invalidate: function(path) {
michael@0 110 // Loops:
michael@0 111 let set = this._loopListeners.get(path);
michael@0 112 if (set) {
michael@0 113 for (let elt of set) {
michael@0 114 this._processLoop(elt);
michael@0 115 }
michael@0 116 }
michael@0 117
michael@0 118 // For:
michael@0 119 set = this._forListeners.get(path);
michael@0 120 if (set) {
michael@0 121 for (let elt of set) {
michael@0 122 this._processFor(elt);
michael@0 123 }
michael@0 124 }
michael@0 125
michael@0 126 // Nodes:
michael@0 127 set = this._nodeListeners.get(path);
michael@0 128 if (set) {
michael@0 129 for (let elt of set) {
michael@0 130 this._processNode(elt);
michael@0 131 }
michael@0 132 }
michael@0 133 },
michael@0 134
michael@0 135 // Delay node registration until the last step of starting / updating the UI.
michael@0 136 // This allows us to avoid doing double work in _storeChanged where the first
michael@0 137 // call to |_invalidate| registers new nodes, which would then be visited a
michael@0 138 // second time when it iterates over node listeners.
michael@0 139 _queueNodeRegistration: function(path, element) {
michael@0 140 this._queuedNodeRegistrations.push([path, element]);
michael@0 141 },
michael@0 142
michael@0 143 _registerQueuedNodes: function() {
michael@0 144 for (let [path, element] of this._queuedNodeRegistrations) {
michael@0 145 // We map a node to a path.
michael@0 146 // If the value behind this path is updated,
michael@0 147 // we get notified from the ObjectEmitter,
michael@0 148 // and then we know which objects to update.
michael@0 149 if (!this._nodeListeners.has(path)) {
michael@0 150 this._nodeListeners.set(path, new Set());
michael@0 151 }
michael@0 152 let set = this._nodeListeners.get(path);
michael@0 153 set.add(element);
michael@0 154 }
michael@0 155 this._queuedNodeRegistrations.length = 0;
michael@0 156 },
michael@0 157
michael@0 158 _unregisterNodes: function(nodes) {
michael@0 159 for (let e of nodes) {
michael@0 160 for (let registeredPath of e.registeredPaths) {
michael@0 161 let set = this._nodeListeners.get(registeredPath);
michael@0 162 if (!set) {
michael@0 163 continue;
michael@0 164 }
michael@0 165 set.delete(e);
michael@0 166 if (set.size === 0) {
michael@0 167 this._nodeListeners.delete(registeredPath);
michael@0 168 }
michael@0 169 }
michael@0 170 e.registeredPaths = null;
michael@0 171 }
michael@0 172 },
michael@0 173
michael@0 174 _registerLoop: function(path, element) {
michael@0 175 if (!this._loopListeners.has(path)) {
michael@0 176 this._loopListeners.set(path, new Set());
michael@0 177 }
michael@0 178 let set = this._loopListeners.get(path);
michael@0 179 set.add(element);
michael@0 180 },
michael@0 181
michael@0 182 _registerFor: function(path, element) {
michael@0 183 if (!this._forListeners.has(path)) {
michael@0 184 this._forListeners.set(path, new Set());
michael@0 185 }
michael@0 186 let set = this._forListeners.get(path);
michael@0 187 set.add(element);
michael@0 188 },
michael@0 189
michael@0 190 _processNode: function(element, resolver=this._rootResolver) {
michael@0 191 // The actual magic.
michael@0 192 // The element has a template attribute.
michael@0 193 // The value is supposed to be a JSON string.
michael@0 194 // resolver is a helper object that is used to retrieve data
michael@0 195 // from the template's data store, give the current path into
michael@0 196 // the data store, or descend down another level of the store.
michael@0 197 // See the Resolver object below.
michael@0 198
michael@0 199 let e = element;
michael@0 200 let str = e.getAttribute("template");
michael@0 201
michael@0 202 try {
michael@0 203 let json = JSON.parse(str);
michael@0 204 // Sanity check
michael@0 205 if (!("type" in json)) {
michael@0 206 throw new Error("missing property");
michael@0 207 }
michael@0 208 if (json.rootPath) {
michael@0 209 // If node has been generated through a loop, we stored
michael@0 210 // previously its rootPath.
michael@0 211 resolver = this._rootResolver.descend(json.rootPath);
michael@0 212 }
michael@0 213
michael@0 214 // paths is an array that will store all the paths we needed
michael@0 215 // to expand the node. We will then, via
michael@0 216 // _registerQueuedNodes, link this element to these paths.
michael@0 217
michael@0 218 let paths = [];
michael@0 219
michael@0 220 switch (json.type) {
michael@0 221 case "attribute": {
michael@0 222 if (!("name" in json) ||
michael@0 223 !("path" in json)) {
michael@0 224 throw new Error("missing property");
michael@0 225 }
michael@0 226 e.setAttribute(json.name, resolver.get(json.path, NOT_FOUND_STRING));
michael@0 227 paths.push(resolver.rootPathTo(json.path));
michael@0 228 break;
michael@0 229 }
michael@0 230 case "textContent": {
michael@0 231 if (!("path" in json)) {
michael@0 232 throw new Error("missing property");
michael@0 233 }
michael@0 234 e.textContent = resolver.get(json.path, NOT_FOUND_STRING);
michael@0 235 paths.push(resolver.rootPathTo(json.path));
michael@0 236 break;
michael@0 237 }
michael@0 238 case "localizedContent": {
michael@0 239 if (!("property" in json) ||
michael@0 240 !("paths" in json)) {
michael@0 241 throw new Error("missing property");
michael@0 242 }
michael@0 243 let params = json.paths.map((p) => {
michael@0 244 paths.push(resolver.rootPathTo(p));
michael@0 245 let str = resolver.get(p, NOT_FOUND_STRING);
michael@0 246 return str;
michael@0 247 });
michael@0 248 e.textContent = this._l10n(json.property, params);
michael@0 249 break;
michael@0 250 }
michael@0 251 }
michael@0 252 if (resolver !== this._rootResolver) {
michael@0 253 // We save the rootPath if any.
michael@0 254 json.rootPath = resolver.path;
michael@0 255 e.setAttribute("template", JSON.stringify(json));
michael@0 256 }
michael@0 257 if (paths.length > 0) {
michael@0 258 for (let path of paths) {
michael@0 259 this._queueNodeRegistration(path, e);
michael@0 260 }
michael@0 261 }
michael@0 262 // Store all the paths on the node, to speed up unregistering later
michael@0 263 e.registeredPaths = paths;
michael@0 264 } catch(exception) {
michael@0 265 console.error("Invalid template: " + e.outerHTML + " (" + exception + ")");
michael@0 266 }
michael@0 267 },
michael@0 268
michael@0 269 _processLoop: function(element, resolver=this._rootResolver) {
michael@0 270 // The element has a template-loop attribute.
michael@0 271 // The related path must be an array. We go
michael@0 272 // through the array, and build one child per
michael@0 273 // item. The template for this child is pointed
michael@0 274 // by the childSelector property.
michael@0 275 let e = element;
michael@0 276 try {
michael@0 277 let template, count;
michael@0 278 let str = e.getAttribute("template-loop");
michael@0 279 let json = JSON.parse(str);
michael@0 280 if (!("arrayPath" in json) ||
michael@0 281 !("childSelector" in json)) {
michael@0 282 throw new Error("missing property");
michael@0 283 }
michael@0 284 let descendedResolver = resolver.descend(json.arrayPath);
michael@0 285 let templateParent = this._doc.querySelector(json.childSelector);
michael@0 286 if (!templateParent) {
michael@0 287 throw new Error("can't find child");
michael@0 288 }
michael@0 289 template = this._doc.createElement("div");
michael@0 290 template.innerHTML = templateParent.innerHTML;
michael@0 291 template = template.firstElementChild;
michael@0 292 let array = descendedResolver.get("", []);
michael@0 293 if (!Array.isArray(array)) {
michael@0 294 console.error("referenced array is not an array");
michael@0 295 }
michael@0 296 count = array.length;
michael@0 297
michael@0 298 let fragment = this._doc.createDocumentFragment();
michael@0 299 for (let i = 0; i < count; i++) {
michael@0 300 let node = template.cloneNode(true);
michael@0 301 this._processTree(node, descendedResolver.descend(i));
michael@0 302 fragment.appendChild(node);
michael@0 303 }
michael@0 304 this._registerLoop(descendedResolver.path, e);
michael@0 305 this._registerLoop(descendedResolver.rootPathTo("length"), e);
michael@0 306 this._unregisterNodes(e.querySelectorAll("[template]"));
michael@0 307 e.innerHTML = "";
michael@0 308 e.appendChild(fragment);
michael@0 309 } catch(exception) {
michael@0 310 console.error("Invalid template: " + e.outerHTML + " (" + exception + ")");
michael@0 311 }
michael@0 312 },
michael@0 313
michael@0 314 _processFor: function(element, resolver=this._rootResolver) {
michael@0 315 let e = element;
michael@0 316 try {
michael@0 317 let template;
michael@0 318 let str = e.getAttribute("template-for");
michael@0 319 let json = JSON.parse(str);
michael@0 320 if (!("path" in json) ||
michael@0 321 !("childSelector" in json)) {
michael@0 322 throw new Error("missing property");
michael@0 323 }
michael@0 324
michael@0 325 if (!json.path) {
michael@0 326 // Nothing to show.
michael@0 327 this._unregisterNodes(e.querySelectorAll("[template]"));
michael@0 328 e.innerHTML = "";
michael@0 329 return;
michael@0 330 }
michael@0 331
michael@0 332 let descendedResolver = resolver.descend(json.path);
michael@0 333 let templateParent = this._doc.querySelector(json.childSelector);
michael@0 334 if (!templateParent) {
michael@0 335 throw new Error("can't find child");
michael@0 336 }
michael@0 337 let content = this._doc.createElement("div");
michael@0 338 content.innerHTML = templateParent.innerHTML;
michael@0 339 content = content.firstElementChild;
michael@0 340
michael@0 341 this._processTree(content, descendedResolver);
michael@0 342
michael@0 343 this._unregisterNodes(e.querySelectorAll("[template]"));
michael@0 344 this._registerFor(descendedResolver.path, e);
michael@0 345
michael@0 346 e.innerHTML = "";
michael@0 347 e.appendChild(content);
michael@0 348
michael@0 349 } catch(exception) {
michael@0 350 console.error("Invalid template: " + e.outerHTML + " (" + exception + ")");
michael@0 351 }
michael@0 352 },
michael@0 353
michael@0 354 _processTree: function(parent, resolver=this._rootResolver) {
michael@0 355 let loops = parent.querySelectorAll(":not(template) [template-loop]");
michael@0 356 let fors = parent.querySelectorAll(":not(template) [template-for]");
michael@0 357 let nodes = parent.querySelectorAll(":not(template) [template]");
michael@0 358 for (let i = 0; i < loops.length; i++) {
michael@0 359 this._processLoop(loops[i], resolver);
michael@0 360 }
michael@0 361 for (let i = 0; i < fors.length; i++) {
michael@0 362 this._processFor(fors[i], resolver);
michael@0 363 }
michael@0 364 for (let i = 0; i < nodes.length; i++) {
michael@0 365 this._processNode(nodes[i], resolver);
michael@0 366 }
michael@0 367 if (parent.hasAttribute("template")) {
michael@0 368 this._processNode(parent, resolver);
michael@0 369 }
michael@0 370 },
michael@0 371 };
michael@0 372
michael@0 373 function Resolver(object, path = "") {
michael@0 374 this._object = object;
michael@0 375 this.path = path;
michael@0 376 }
michael@0 377
michael@0 378 Resolver.prototype = {
michael@0 379
michael@0 380 get: function(path, defaultValue = null) {
michael@0 381 let obj = this._object;
michael@0 382 if (path === "") {
michael@0 383 return obj;
michael@0 384 }
michael@0 385 let chunks = path.toString().split(".");
michael@0 386 for (let i = 0; i < chunks.length; i++) {
michael@0 387 let word = chunks[i];
michael@0 388 if ((typeof obj) == "object" &&
michael@0 389 (word in obj)) {
michael@0 390 obj = obj[word];
michael@0 391 } else {
michael@0 392 return defaultValue;
michael@0 393 }
michael@0 394 }
michael@0 395 return obj;
michael@0 396 },
michael@0 397
michael@0 398 rootPathTo: function(path) {
michael@0 399 return this.path ? this.path + "." + path : path;
michael@0 400 },
michael@0 401
michael@0 402 descend: function(path) {
michael@0 403 return new Resolver(this.get(path), this.rootPathTo(path));
michael@0 404 }
michael@0 405
michael@0 406 };

mercurial