]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/overlay.jsm
Import r6948 from upstream hg supporting Firefox up to 24.*
[dactyl.git] / common / modules / overlay.jsm
1 // Copyright (c) 2009-2012 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(function () []),
31     objects: Class.Memoize(function () ({})),
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(function () 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             if (target == null || args[0].get() == target && args[1] == event && args[2].wrapped == callback && args[3] == capture) {
107                 args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
108                 return false;
109             }
110             return !args[0].get();
111         }));
112     },
113
114     cleanup: function cleanup(reason) {
115         for (let doc in util.iterDocuments()) {
116             for (let elem in values(this.getData(doc, "overlayElements")))
117                 if (elem.parentNode)
118                     elem.parentNode.removeChild(elem);
119
120             for (let [elem, ns, name, orig, value] in values(this.getData(doc, "overlayAttributes")))
121                 if (getAttr(elem, ns, name) === value)
122                     setAttr(elem, ns, name, orig);
123
124             for (let callback in values(this.getData(doc, "cleanup")))
125                 util.trapErrors(callback, doc, reason);
126
127             this.unlisten(doc, true);
128
129             delete doc[this.id];
130             delete doc.defaultView[this.id];
131         }
132     },
133
134     observers: {
135         "toplevel-window-ready": function (window, data) {
136             let listener = util.wrapCallback(function listener(event) {
137                 if (event.originalTarget === window.document) {
138                     window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
139                     window.removeEventListener("load", listener.wrapper, true);
140                     overlay._loadOverlays(window);
141                 }
142             });
143
144             window.addEventListener("DOMContentLoaded", listener, true);
145             window.addEventListener("load", listener, true);
146         },
147         "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
148         "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
149         "xul-window-visible": function () {
150             if (this.onWindowVisible)
151                 this.onWindowVisible.forEach(function (f) f.call(this), this);
152             this.onWindowVisible = null;
153         }
154     },
155
156     getData: function getData(obj, key, constructor) {
157
158         if (!this.weakMap.has(obj))
159             try {
160                 this.weakMap.set(obj, {});
161             }
162             catch (e if e instanceof TypeError) {
163                 // util.dump("Bad WeakMap key: " + obj + " " + Components.stack.caller);
164                 let { id } = this;
165
166                 if (!(id in obj && obj[id]))
167                     obj[id] = {};
168
169                 var data = obj[id];
170             }
171
172         data = data || this.weakMap.get(obj);
173
174         if (arguments.length == 1)
175             return data;
176
177         if (data[key] === undefined)
178             if (constructor === undefined || callable(constructor))
179                 data[key] = (constructor || Array)();
180             else
181                 data[key] = constructor;
182
183         return data[key];
184     },
185
186     setData: function setData(obj, key, val) {
187         let data = this.getData(obj);
188
189         return data[key] = val;
190     },
191
192     overlayWindow: function (url, fn) {
193         if (url instanceof Ci.nsIDOMWindow)
194             overlay._loadOverlay(url, fn);
195         else {
196             Array.concat(url).forEach(function (url) {
197                 if (!this.overlays[url])
198                     this.overlays[url] = [];
199                 this.overlays[url].push(fn);
200             }, this);
201
202             for (let doc in util.iterDocuments())
203                 if (~["interactive", "complete"].indexOf(doc.readyState)) {
204                     this.observe(doc.defaultView, "xul-window-visible");
205                     this._loadOverlays(doc.defaultView);
206                 }
207                 else {
208                     if (!this.onWindowVisible)
209                         this.onWindowVisible = [];
210                     this.observe(doc.defaultView, "toplevel-window-ready");
211                 }
212         }
213     },
214
215     _loadOverlays: function _loadOverlays(window) {
216         let overlays = this.getData(window, "overlays");
217
218         for each (let obj in overlay.overlays[window.document.documentURI] || []) {
219             if (~overlays.indexOf(obj))
220                 continue;
221             overlays.push(obj);
222             this._loadOverlay(window, obj(window));
223         }
224     },
225
226     _loadOverlay: function _loadOverlay(window, obj) {
227         let doc = window.document;
228         let savedElems = this.getData(doc, "overlayElements");
229         let savedAttrs = this.getData(doc, "overlayAttributes");
230
231         function insert(key, fn) {
232             if (obj[key]) {
233                 let iterator = Iterator(obj[key]);
234                 if (isArray(obj[key])) {
235                     iterator = ([elem[1].id, elem.slice(2), elem[1]]
236                                 for each (elem in obj[key]));
237                 }
238
239                 for (let [elem, xml, attrs] in iterator) {
240                     if (elem = doc.getElementById(String(elem))) {
241                         // Urgh. Hack.
242                         let namespaces;
243                         if (attrs && !isXML(attrs))
244                             namespaces = iter([k.slice(6), DOM.fromJSON.namespaces[v] || v]
245                                               for ([k, v] in Iterator(attrs))
246                                               if (/^xmlns(?:$|:)/.test(k))).toObject();
247
248                         let node;
249                         if (isXML(xml))
250                             node = DOM.fromXML(xml, doc, obj.objects);
251                         else
252                             node = DOM.fromJSON(xml, doc, obj.objects, namespaces);
253
254                         if (!(node instanceof Ci.nsIDOMDocumentFragment))
255                             savedElems.push(node);
256                         else
257                             for (let n in array.iterValues(node.childNodes))
258                                 savedElems.push(n);
259
260                         fn(elem, node);
261
262                         if (isXML(attrs))
263                             // Evilness and such.
264                             let (oldAttrs = attrs) {
265                                 attrs = (attr for each (attr in oldAttrs));
266                             }
267
268                         for (let attr in attrs || []) {
269                             let [ns, localName] = DOM.parseNamespace(attr);
270                             let name = attr;
271                             let val = attrs[attr];
272
273                             savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]);
274                             if (name === "highlight")
275                                 highlight.highlightNode(elem, val);
276                             else
277                                 elem.setAttributeNS(ns || "", name, val);
278                         }
279                     }
280                 }
281             }
282         }
283
284         insert("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
285         insert("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
286         insert("append", function (elem, dom) elem.appendChild(dom));
287         insert("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
288         if (obj.ready)
289             util.trapErrors("ready", obj, window);
290
291         function load(event) {
292             util.trapErrors("load", obj, window, event);
293             if (obj.visible)
294                 if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
295                     util.trapErrors("visible", obj, window);
296                 else
297                     overlay.onWindowVisible.push(function () { obj.visible(window) });
298         }
299
300         if (obj.load)
301             if (doc.readyState === "complete")
302                 load();
303             else
304                 window.addEventListener("load", util.wrapCallback(function onLoad(event) {
305                     if (event.originalTarget === doc) {
306                         window.removeEventListener("load", onLoad.wrapper, true);
307                         load(event);
308                     }
309                 }), true);
310
311         if (obj.unload || obj.cleanup)
312             this.listen(window, "unload", function unload(event) {
313                 if (event.originalTarget === doc) {
314                     overlay.unlisten(window, "unload", unload);
315                     if (obj.unload)
316                         util.trapErrors("unload", obj, window, event);
317
318                     if (obj.cleanup)
319                         util.trapErrors("cleanup", obj, window, "unload", event);
320                 }
321             });
322
323         if (obj.cleanup)
324             this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
325     },
326
327     /**
328      * Overlays an object with the given property overrides. Each
329      * property in *overrides* is added to *object*, replacing any
330      * original value. Functions in *overrides* are augmented with the
331      * new properties *super*, *supercall*, and *superapply*, in the
332      * same manner as class methods, so that they may call their
333      * overridden counterparts.
334      *
335      * @param {object} object The object to overlay.
336      * @param {object} overrides An object containing properties to
337      *      override.
338      * @returns {function} A function which, when called, will remove
339      *      the overlay.
340      */
341     overlayObject: function (object, overrides) {
342         let original = Object.create(object);
343         overrides = update(Object.create(original), overrides);
344
345         Object.getOwnPropertyNames(overrides).forEach(function (k) {
346             let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
347             if (desc.value instanceof Class.Property)
348                 desc = desc.value.init(k) || desc.value;
349
350             if (k in object) {
351                 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
352                     if (orig = Object.getOwnPropertyDescriptor(obj, k))
353                         Object.defineProperty(original, k, orig);
354
355                 if (!orig)
356                     if (orig = Object.getPropertyDescriptor(object, k))
357                         Object.defineProperty(original, k, orig);
358             }
359
360             // Guard against horrible add-ons that use eval-based monkey
361             // patching.
362             let value = desc.value;
363             if (callable(desc.value)) {
364
365                 delete desc.value;
366                 delete desc.writable;
367                 desc.get = function get() value;
368                 desc.set = function set(val) {
369                     if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
370                         Class.replaceProperty(this, k, val);
371                     else {
372                         let package_ = util.newURI(Components.stack.caller.filename).host;
373                         util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
374                         util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
375                     }
376                 };
377             }
378
379             try {
380                 Object.defineProperty(object, k, desc);
381
382                 if (callable(value)) {
383                     var sentinel = "(function DactylOverlay() {}())";
384                     value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
385                     value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
386                 }
387             }
388             catch (e) {
389                 try {
390                     if (value) {
391                         object[k] = value;
392                         return;
393                     }
394                 }
395                 catch (f) {}
396                 util.reportError(e);
397             }
398         }, this);
399
400         return function unwrap() {
401             for each (let k in Object.getOwnPropertyNames(original))
402                 if (Object.getOwnPropertyDescriptor(object, k).configurable)
403                     Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
404                 else {
405                     try {
406                         object[k] = original[k];
407                     }
408                     catch (e) {}
409                 }
410         };
411     },
412
413     get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
414
415     get modules() this.windows.map(function (w) w.dactyl.modules),
416
417     /**
418      * The most recently active dactyl window.
419      */
420     get activeWindow() this.windows[0],
421
422     set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
423
424     /**
425      * A list of extant dactyl windows.
426      */
427     windows: Class.Memoize(function () [])
428 });
429
430 endModule();
431
432 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
433
434 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: