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.

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

mercurial