Wed, 31 Dec 2014 06:09:35 +0100
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 | }; |