]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/overlay.jsm
finalize changelog for 7904
[dactyl.git] / common / modules / overlay.jsm
1 // Copyright (c) 2009-2014 Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 try {
8
9 defineModule("overlay", {
10     exports: ["overlay"],
11     require: ["util"]
12 });
13
14 lazyRequire("highlight", ["highlight"]);
15
16 var getAttr = function getAttr(elem, ns, name)
17     elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
18 var setAttr = function setAttr(elem, ns, name, val) {
19     if (val == null)
20         elem.removeAttributeNS(ns, name);
21     else
22         elem.setAttributeNS(ns, name, val);
23 };
24
25 var Overlay = Class("Overlay", {
26     init: function init(window) {
27         this.window = window;
28     },
29
30     cleanups: Class.Memoize(() => []),
31     objects: Class.Memoize(() => ({})),
32
33     get doc() this.window.document,
34
35     get win() this.window,
36
37     $: function $(sel, node) DOM(sel, node || this.doc),
38
39     cleanup: function cleanup(window, reason) {
40         for (let fn in values(this.cleanups))
41             util.trapErrors(fn, this, window, reason);
42     }
43 });
44
45 var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
46     init: function init() {
47         util.addObserver(this);
48         this.overlays = {};
49
50         this.weakMap = WeakMap();
51
52         this.onWindowVisible = [];
53     },
54
55     id: Class.Memoize(() => config.addon.id),
56
57     /**
58      * Adds an event listener for this session and removes it on
59      * dactyl shutdown.
60      *
61      * @param {Element} target The element on which to listen.
62      * @param {string} event The event to listen for.
63      * @param {function} callback The function to call when the event is received.
64      * @param {boolean} capture When true, listen during the capture
65      *      phase, otherwise during the bubbling phase.
66      * @param {boolean} allowUntrusted When true, allow capturing of
67      *      untrusted events.
68      */
69     listen: function (target, event, callback, capture, allowUntrusted) {
70         let doc = target.ownerDocument || target.document || target;
71         let listeners = this.getData(doc, "listeners");
72
73         if (!isObject(event))
74             var [self, events] = [null, array.toObject([[event, callback]])];
75         else
76             [self, events] = [event, event[callback || "events"]];
77
78         for (let [event, callback] in Iterator(events)) {
79             let args = [util.weakReference(target),
80                         event,
81                         util.wrapCallback(callback, self),
82                         capture,
83                         allowUntrusted];
84
85             target.addEventListener.apply(target, args.slice(1));
86             listeners.push(args);
87         }
88     },
89
90     /**
91      * Remove an event listener.
92      *
93      * @param {Element} target The element on which to listen.
94      * @param {string} event The event to listen for.
95      * @param {function} callback The function to call when the event is received.
96      * @param {boolean} capture When true, listen during the capture
97      *      phase, otherwise during the bubbling phase.
98      */
99     unlisten: function (target, event, callback, capture) {
100         let doc = target.ownerDocument || target.document || target;
101         let listeners = this.getData(doc, "listeners");
102         if (event === true)
103             target = null;
104
105         this.setData(doc, "listeners", listeners.filter(function (args) {
106             let elem = args[0].get();
107             if (target == null || elem == target && args[1] == event && args[2].wrapped == callback && args[3] == capture) {
108                 elem.removeEventListener.apply(elem, args.slice(1));
109                 return false;
110             }
111             return elem;
112         }));
113     },
114
115     cleanup: function cleanup(reason) {
116         for (let doc in util.iterDocuments()) {
117             for (let callback in values(this.getData(doc, "cleanup")))
118                 util.trapErrors(callback, doc, reason);
119
120             for (let elem in values(this.getData(doc, "overlayElements")))
121                 if (elem.parentNode)
122                     elem.parentNode.removeChild(elem);
123
124             for (let [elem, ns, name, orig, value] in values(this.getData(doc, "overlayAttributes")))
125                 if (getAttr(elem, ns, name) === value)
126                     setAttr(elem, ns, name, orig);
127
128             this.unlisten(doc, true);
129
130             delete doc[this.id];
131             delete doc.defaultView[this.id];
132         }
133     },
134
135     observers: {
136         "toplevel-window-ready": function (window, data) {
137             let listener = util.wrapCallback(function listener(event) {
138                 if (event.originalTarget === window.document) {
139                     window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
140                     window.removeEventListener("load", listener.wrapper, true);
141                     overlay._loadOverlays(window);
142                 }
143             });
144
145             window.addEventListener("DOMContentLoaded", listener, true);
146             window.addEventListener("load", listener, true);
147         },
148         "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
149         "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
150         "xul-window-visible": function () {
151             if (this.onWindowVisible)
152                 this.onWindowVisible.forEach(f => { f.call(this); });
153             this.onWindowVisible = null;
154         }
155     },
156
157     getData: function getData(obj, key, constructor) {
158
159         if (!this.weakMap.has(obj))
160             try {
161                 this.weakMap.set(obj, {});
162             }
163             catch (e if e instanceof TypeError) {
164                 // util.dump("Bad WeakMap key: " + obj + " " + Components.stack.caller);
165                 let { id } = this;
166
167                 if (!(id in obj && obj[id]))
168                     obj[id] = {};
169
170                 var data = obj[id];
171             }
172
173         data = data || this.weakMap.get(obj);
174
175         if (arguments.length == 1)
176             return data;
177
178         if (data[key] === undefined)
179             if (constructor === undefined || callable(constructor))
180                 data[key] = (constructor || Array)();
181             else
182                 data[key] = constructor;
183
184         return data[key];
185     },
186
187     setData: function setData(obj, key, val) {
188         let data = this.getData(obj);
189         if (val !== undefined)
190             return data[key] = val;
191
192         delete data[key];
193     },
194
195     overlayWindow: function overlayWindow(url, fn) {
196         if (url instanceof Ci.nsIDOMWindow)
197             overlay._loadOverlay(url, fn);
198         else {
199             Array.concat(url).forEach(function (url) {
200                 if (!this.overlays[url])
201                     this.overlays[url] = [];
202                 this.overlays[url].push(fn);
203             }, this);
204
205             for (let doc in util.iterDocuments())
206                 if (~["interactive", "complete"].indexOf(doc.readyState)) {
207                     this.observe(doc.defaultView, "xul-window-visible");
208                     this._loadOverlays(doc.defaultView);
209                 }
210                 else {
211                     if (!this.onWindowVisible)
212                         this.onWindowVisible = [];
213                     this.observe(doc.defaultView, "toplevel-window-ready");
214                 }
215         }
216     },
217
218     _loadOverlays: function _loadOverlays(window) {
219         let overlays = this.getData(window, "overlays");
220
221         for (let obj of overlay.overlays[window.document.documentURI] || []) {
222             if (~overlays.indexOf(obj))
223                 continue;
224             overlays.push(obj);
225             this._loadOverlay(window, obj(window));
226         }
227     },
228
229     _loadOverlay: function _loadOverlay(window, obj) {
230         let doc = window.document;
231         let savedElems = this.getData(doc, "overlayElements");
232         let savedAttrs = this.getData(doc, "overlayAttributes");
233
234         function insert(key, fn) {
235             if (obj[key]) {
236                 let iterator = Iterator(obj[key]);
237                 if (isArray(obj[key])) {
238                     iterator = ([elem[1].id, elem.slice(2), elem[1]]
239                                 for each (elem in obj[key]));
240                 }
241
242                 for (let [elem, xml, attrs] in iterator) {
243                     if (elem = doc.getElementById(String(elem))) {
244                         // Urgh. Hack.
245                         let namespaces;
246                         if (attrs)
247                             namespaces = iter([k.slice(6), DOM.fromJSON.namespaces[v] || v]
248                                               for ([k, v] in Iterator(attrs))
249                                               if (/^xmlns(?:$|:)/.test(k))).toObject();
250
251                         let node = DOM.fromJSON(xml, doc, obj.objects, namespaces);
252
253                         if (!(node instanceof Ci.nsIDOMDocumentFragment))
254                             savedElems.push(node);
255                         else
256                             for (let n in array.iterValues(node.childNodes))
257                                 savedElems.push(n);
258
259                         fn(elem, node);
260
261                         for (let attr in attrs || []) {
262                             let [ns, localName] = DOM.parseNamespace(attr);
263                             let name = attr;
264                             let val = attrs[attr];
265
266                             savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]);
267                             if (name === "highlight")
268                                 highlight.highlightNode(elem, val);
269                             else
270                                 elem.setAttributeNS(ns || "", name, val);
271                         }
272                     }
273                 }
274             }
275         }
276
277         insert("before", (elem, dom) => elem.parentNode.insertBefore(dom, elem));
278         insert("after", (elem, dom) => elem.parentNode.insertBefore(dom, elem.nextSibling));
279         insert("append", (elem, dom) => elem.appendChild(dom));
280         insert("prepend", (elem, dom) => elem.insertBefore(dom, elem.firstChild));
281         if (obj.ready)
282             util.trapErrors("ready", obj, window);
283
284         function load(event) {
285             util.trapErrors("load", obj, window, event);
286             if (obj.visible)
287                 if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
288                     util.trapErrors("visible", obj, window);
289                 else
290                     overlay.onWindowVisible.push(function () { obj.visible(window) });
291         }
292
293         if (obj.load)
294             if (doc.readyState === "complete")
295                 load();
296             else
297                 window.addEventListener("load", util.wrapCallback(function onLoad(event) {
298                     if (event.originalTarget === doc) {
299                         window.removeEventListener("load", onLoad.wrapper, true);
300                         load(event);
301                     }
302                 }), true);
303
304         if (obj.unload || obj.cleanup)
305             this.listen(window, "unload", function unload(event) {
306                 if (event.originalTarget === doc) {
307                     overlay.unlisten(window, "unload", unload);
308                     if (obj.unload)
309                         util.trapErrors("unload", obj, window, event);
310
311                     if (obj.cleanup)
312                         util.trapErrors("cleanup", obj, window, "unload", event);
313                 }
314             });
315
316         if (obj.cleanup)
317             this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
318     },
319
320     /**
321      * Overlays an object with the given property overrides. Each
322      * property in *overrides* is added to *object*, replacing any
323      * original value. Functions in *overrides* are augmented with the
324      * new properties *super*, *supercall*, and *superapply*, in the
325      * same manner as class methods, so that they may call their
326      * overridden counterparts.
327      *
328      * @param {object} object The object to overlay.
329      * @param {object} overrides An object containing properties to
330      *      override.
331      * @returns {function} A function which, when called, will remove
332      *      the overlay.
333      */
334     overlayObject: function (object, overrides) {
335         let original = Object.create(object);
336         overrides = update(Object.create(original), overrides);
337
338         Object.getOwnPropertyNames(overrides).forEach(function (k) {
339             let desc = Object.getOwnPropertyDescriptor(overrides, k);
340
341             if (desc.value instanceof Class.Property)
342                 desc = desc.value.init(k) || desc.value;
343
344             if (k in object) {
345                 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj)) {
346                     var orig = Object.getOwnPropertyDescriptor(obj, k);
347                     if (orig)
348                         Object.defineProperty(original, k, orig);
349                 }
350
351                 if (!orig) {
352                     orig = Object.getPropertyDescriptor(object, k);
353                     if (orig)
354                         Object.defineProperty(original, k, orig);
355                 }
356             }
357
358             // Guard against horrible add-ons that use eval-based monkey
359             // patching.
360             let value = desc.value;
361             if (callable(desc.value)) {
362
363                 delete desc.value;
364                 delete desc.writable;
365                 desc.get = function get() value;
366                 desc.set = function set(val) {
367                     if (!callable(val) || !Function.prototype.toString(val).contains(sentinel))
368                         Class.replaceProperty(this, k, val);
369                     else {
370                         let package_ = util.newURI(Components.stack.caller.filename).host;
371                         util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
372                         util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
373                     }
374                 };
375             }
376
377             try {
378                 Object.defineProperty(object, k, desc);
379
380                 if (callable(value)) {
381                     var sentinel = "(function DactylOverlay() {}())";
382                     value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
383                     value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
384                 }
385             }
386             catch (e) {
387                 try {
388                     if (value) {
389                         object[k] = value;
390                         return;
391                     }
392                 }
393                 catch (f) {}
394                 util.reportError(e);
395             }
396         }, this);
397
398         return function unwrap() {
399             for (let k of Object.getOwnPropertyNames(original))
400                 if (Object.getOwnPropertyDescriptor(object, k).configurable)
401                     Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
402                 else {
403                     try {
404                         object[k] = original[k];
405                     }
406                     catch (e) {}
407                 }
408         };
409     },
410
411     get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
412
413     get modules() [w.dactyl.modules for (w of this.windows)],
414
415     /**
416      * The most recently active dactyl window.
417      */
418     get activeWindow() {
419         let win = this._activeWindow && this._activeWindow.get();
420         return this.windows.has(win) && win;
421     },
422
423     set activeWindow(win) this._activeWindow = util.weakReference(win),
424
425     /**
426      * A list of extant dactyl windows.
427      */
428     windows: Class.Memoize(() => RealSet())
429 });
430
431 endModule();
432
433 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
434
435 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: