]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/overlay.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[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
46 var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
47     init: function init() {
48         util.addObserver(this);
49         this.overlays = {};
50
51         this.weakMap = WeakMap();
52
53         this.onWindowVisible = [];
54     },
55
56     id: Class.Memoize(function () config.addon.id),
57
58     /**
59      * Adds an event listener for this session and removes it on
60      * dactyl shutdown.
61      *
62      * @param {Element} target The element on which to listen.
63      * @param {string} event The event to listen for.
64      * @param {function} callback The function to call when the event is received.
65      * @param {boolean} capture When true, listen during the capture
66      *      phase, otherwise during the bubbling phase.
67      * @param {boolean} allowUntrusted When true, allow capturing of
68      *      untrusted events.
69      */
70     listen: function (target, event, callback, capture, allowUntrusted) {
71         let doc = target.ownerDocument || target.document || target;
72         let listeners = this.getData(doc, "listeners");
73
74         if (!isObject(event))
75             var [self, events] = [null, array.toObject([[event, callback]])];
76         else
77             [self, events] = [event, event[callback || "events"]];
78
79         for (let [event, callback] in Iterator(events)) {
80             let args = [util.weakReference(target),
81                         event,
82                         util.wrapCallback(callback, self),
83                         capture,
84                         allowUntrusted];
85
86             target.addEventListener.apply(target, args.slice(1));
87             listeners.push(args);
88         }
89     },
90
91     /**
92      * Remove an event listener.
93      *
94      * @param {Element} target The element on which to listen.
95      * @param {string} event The event to listen for.
96      * @param {function} callback The function to call when the event is received.
97      * @param {boolean} capture When true, listen during the capture
98      *      phase, otherwise during the bubbling phase.
99      */
100     unlisten: function (target, event, callback, capture) {
101         let doc = target.ownerDocument || target.document || target;
102         let listeners = this.getData(doc, "listeners");
103         if (event === true)
104             target = null;
105
106         this.setData(doc, "listeners", listeners.filter(function (args) {
107             if (target == null || args[0].get() == target && args[1] == event && args[2].wrapped == callback && args[3] == capture) {
108                 args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
109                 return false;
110             }
111             return !args[0].get();
112         }));
113     },
114
115     cleanup: function cleanup(reason) {
116         for (let doc in util.iterDocuments()) {
117             for (let elem in values(this.getData(doc, "overlayElements")))
118                 if (elem.parentNode)
119                     elem.parentNode.removeChild(elem);
120
121             for (let [elem, ns, name, orig, value] in values(this.getData(doc, "overlayAttributes")))
122                 if (getAttr(elem, ns, name) === value)
123                     setAttr(elem, ns, name, orig);
124
125             for (let callback in values(this.getData(doc, "cleanup")))
126                 util.trapErrors(callback, doc, reason);
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(function (f) f.call(this), 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
190         return data[key] = val;
191     },
192
193     overlayWindow: function (url, fn) {
194         if (url instanceof Ci.nsIDOMWindow)
195             overlay._loadOverlay(url, fn);
196         else {
197             Array.concat(url).forEach(function (url) {
198                 if (!this.overlays[url])
199                     this.overlays[url] = [];
200                 this.overlays[url].push(fn);
201             }, this);
202
203             for (let doc in util.iterDocuments())
204                 if (~["interactive", "complete"].indexOf(doc.readyState)) {
205                     this.observe(doc.defaultView, "xul-window-visible");
206                     this._loadOverlays(doc.defaultView);
207                 }
208                 else {
209                     if (!this.onWindowVisible)
210                         this.onWindowVisible = [];
211                     this.observe(doc.defaultView, "toplevel-window-ready");
212                 }
213         }
214     },
215
216     _loadOverlays: function _loadOverlays(window) {
217         let overlays = this.getData(window, "overlays");
218
219         for each (let obj in overlay.overlays[window.document.documentURI] || []) {
220             if (~overlays.indexOf(obj))
221                 continue;
222             overlays.push(obj);
223             this._loadOverlay(window, obj(window));
224         }
225     },
226
227     _loadOverlay: function _loadOverlay(window, obj) {
228         let doc = window.document;
229         let savedElems = this.getData(doc, "overlayElements");
230         let savedAttrs = this.getData(doc, "overlayAttributes");
231
232         function insert(key, fn) {
233             if (obj[key]) {
234                 let iterator = Iterator(obj[key]);
235                 if (isArray(obj[key])) {
236                     iterator = ([elem[1].id, elem.slice(2), elem[1]]
237                                 for each (elem in obj[key]))
238                 }
239
240                 for (let [elem, xml, attrs] in iterator) {
241                     if (elem = doc.getElementById(String(elem))) {
242                         // Urgh. Hack.
243                         let namespaces;
244                         if (attrs && !isXML(attrs))
245                             namespaces = iter([k.slice(6), DOM.fromJSON.namespaces[v] || v]
246                                               for ([k, v] in Iterator(attrs))
247                                               if (/^xmlns(?:$|:)/.test(k))).toObject();
248
249                         let node;
250                         if (isXML(xml))
251                             node = DOM.fromXML(xml, doc, obj.objects);
252                         else
253                             node = DOM.fromJSON(xml, doc, obj.objects, namespaces);
254
255                         if (!(node instanceof Ci.nsIDOMDocumentFragment))
256                             savedElems.push(node);
257                         else
258                             for (let n in array.iterValues(node.childNodes))
259                                 savedElems.push(n);
260
261                         fn(elem, node);
262
263                         if (isXML(attrs))
264                             // Evilness and such.
265                             let (oldAttrs = attrs) {
266                                 attrs = (attr for each (attr in oldAttrs));
267                             }
268
269                         for (let attr in attrs || []) {
270                             let [ns, localName] = DOM.parseNamespace(attr);
271                             let name = attr;
272                             let val = attrs[attr];
273
274                             savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]);
275                             if (name === "highlight")
276                                 highlight.highlightNode(elem, val);
277                             else
278                                 elem.setAttributeNS(ns || "", name, val);
279                         }
280                     }
281                 }
282             }
283         }
284
285         insert("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
286         insert("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
287         insert("append", function (elem, dom) elem.appendChild(dom));
288         insert("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
289         if (obj.ready)
290             util.trapErrors("ready", obj, window);
291
292         function load(event) {
293             util.trapErrors("load", obj, window, event);
294             if (obj.visible)
295                 if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
296                     util.trapErrors("visible", obj, window);
297                 else
298                     overlay.onWindowVisible.push(function () { obj.visible(window) });
299         }
300
301         if (obj.load)
302             if (doc.readyState === "complete")
303                 load();
304             else
305                 window.addEventListener("load", util.wrapCallback(function onLoad(event) {
306                     if (event.originalTarget === doc) {
307                         window.removeEventListener("load", onLoad.wrapper, true);
308                         load(event);
309                     }
310                 }), true);
311
312         if (obj.unload || obj.cleanup)
313             this.listen(window, "unload", function unload(event) {
314                 if (event.originalTarget === doc) {
315                     overlay.unlisten(window, "unload", unload);
316                     if (obj.unload)
317                         util.trapErrors("unload", obj, window, event);
318
319                     if (obj.cleanup)
320                         util.trapErrors("cleanup", obj, window, "unload", event);
321                 }
322             });
323
324         if (obj.cleanup)
325             this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
326     },
327
328     /**
329      * Overlays an object with the given property overrides. Each
330      * property in *overrides* is added to *object*, replacing any
331      * original value. Functions in *overrides* are augmented with the
332      * new properties *super*, *supercall*, and *superapply*, in the
333      * same manner as class methods, so that they may call their
334      * overridden counterparts.
335      *
336      * @param {object} object The object to overlay.
337      * @param {object} overrides An object containing properties to
338      *      override.
339      * @returns {function} A function which, when called, will remove
340      *      the overlay.
341      */
342     overlayObject: function (object, overrides) {
343         let original = Object.create(object);
344         overrides = update(Object.create(original), overrides);
345
346         Object.getOwnPropertyNames(overrides).forEach(function (k) {
347             let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
348             if (desc.value instanceof Class.Property)
349                 desc = desc.value.init(k) || desc.value;
350
351             if (k in object) {
352                 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
353                     if (orig = Object.getOwnPropertyDescriptor(obj, k))
354                         Object.defineProperty(original, k, orig);
355
356                 if (!orig)
357                     if (orig = Object.getPropertyDescriptor(object, k))
358                         Object.defineProperty(original, k, orig);
359             }
360
361             // Guard against horrible add-ons that use eval-based monkey
362             // patching.
363             let value = desc.value;
364             if (callable(desc.value)) {
365
366                 delete desc.value;
367                 delete desc.writable;
368                 desc.get = function get() value;
369                 desc.set = function set(val) {
370                     if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
371                         Class.replaceProperty(this, k, val);
372                     else {
373                         let package_ = util.newURI(Components.stack.caller.filename).host;
374                         util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
375                         util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
376                     }
377                 };
378             }
379
380             try {
381                 Object.defineProperty(object, k, desc);
382
383                 if (callable(value)) {
384                     var sentinel = "(function DactylOverlay() {}())"
385                     value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
386                     value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
387                 }
388             }
389             catch (e) {
390                 try {
391                     if (value) {
392                         object[k] = value;
393                         return;
394                     }
395                 }
396                 catch (f) {}
397                 util.reportError(e);
398             }
399         }, this);
400
401         return function unwrap() {
402             for each (let k in Object.getOwnPropertyNames(original))
403                 if (Object.getOwnPropertyDescriptor(object, k).configurable)
404                     Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
405                 else {
406                     try {
407                         object[k] = original[k];
408                     }
409                     catch (e) {}
410                 }
411         };
412     },
413
414     get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
415
416     get modules() this.windows.map(function (w) w.dactyl.modules),
417
418     /**
419      * The most recently active dactyl window.
420      */
421     get activeWindow() this.windows[0],
422
423     set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
424
425     /**
426      * A list of extant dactyl windows.
427      */
428     windows: Class.Memoize(function () [])
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 ts=4 et ft=javascript: