1 // Copyright (c) 2009-2012 Kris Maglione <maglione.k@gmail.com>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
9 defineModule("overlay", {
14 lazyRequire("highlight", ["highlight"]);
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) {
20 elem.removeAttributeNS(ns, name);
22 elem.setAttributeNS(ns, name, val);
25 var Overlay = Class("Overlay", {
26 init: function init(window) {
30 cleanups: Class.Memoize(function () []),
31 objects: Class.Memoize(function () ({})),
33 get doc() this.window.document,
35 get win() this.window,
37 $: function $(sel, node) DOM(sel, node || this.doc),
39 cleanup: function cleanup(window, reason) {
40 for (let fn in values(this.cleanups))
41 util.trapErrors(fn, this, window, reason);
45 var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
46 init: function init() {
47 util.addObserver(this);
50 this.weakMap = WeakMap();
52 this.onWindowVisible = [];
55 id: Class.Memoize(function () config.addon.id),
58 * Adds an event listener for this session and removes it on
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
69 listen: function (target, event, callback, capture, allowUntrusted) {
70 let doc = target.ownerDocument || target.document || target;
71 let listeners = this.getData(doc, "listeners");
74 var [self, events] = [null, array.toObject([[event, callback]])];
76 [self, events] = [event, event[callback || "events"]];
78 for (let [event, callback] in Iterator(events)) {
79 let args = [util.weakReference(target),
81 util.wrapCallback(callback, self),
85 target.addEventListener.apply(target, args.slice(1));
91 * Remove an event listener.
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.
99 unlisten: function (target, event, callback, capture) {
100 let doc = target.ownerDocument || target.document || target;
101 let listeners = this.getData(doc, "listeners");
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));
110 return !args[0].get();
114 cleanup: function cleanup(reason) {
115 for (let doc in util.iterDocuments()) {
116 for (let elem in values(this.getData(doc, "overlayElements")))
118 elem.parentNode.removeChild(elem);
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);
124 for (let callback in values(this.getData(doc, "cleanup")))
125 util.trapErrors(callback, doc, reason);
127 this.unlisten(doc, true);
130 delete doc.defaultView[this.id];
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);
144 window.addEventListener("DOMContentLoaded", listener, true);
145 window.addEventListener("load", listener, true);
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;
156 getData: function getData(obj, key, constructor) {
158 if (!this.weakMap.has(obj))
160 this.weakMap.set(obj, {});
162 catch (e if e instanceof TypeError) {
163 // util.dump("Bad WeakMap key: " + obj + " " + Components.stack.caller);
166 if (!(id in obj && obj[id]))
172 data = data || this.weakMap.get(obj);
174 if (arguments.length == 1)
177 if (data[key] === undefined)
178 if (constructor === undefined || callable(constructor))
179 data[key] = (constructor || Array)();
181 data[key] = constructor;
186 setData: function setData(obj, key, val) {
187 let data = this.getData(obj);
189 return data[key] = val;
192 overlayWindow: function (url, fn) {
193 if (url instanceof Ci.nsIDOMWindow)
194 overlay._loadOverlay(url, fn);
196 Array.concat(url).forEach(function (url) {
197 if (!this.overlays[url])
198 this.overlays[url] = [];
199 this.overlays[url].push(fn);
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);
208 if (!this.onWindowVisible)
209 this.onWindowVisible = [];
210 this.observe(doc.defaultView, "toplevel-window-ready");
215 _loadOverlays: function _loadOverlays(window) {
216 let overlays = this.getData(window, "overlays");
218 for each (let obj in overlay.overlays[window.document.documentURI] || []) {
219 if (~overlays.indexOf(obj))
222 this._loadOverlay(window, obj(window));
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");
231 function insert(key, fn) {
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]));
239 for (let [elem, xml, attrs] in iterator) {
240 if (elem = doc.getElementById(String(elem))) {
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();
250 node = DOM.fromXML(xml, doc, obj.objects);
252 node = DOM.fromJSON(xml, doc, obj.objects, namespaces);
254 if (!(node instanceof Ci.nsIDOMDocumentFragment))
255 savedElems.push(node);
257 for (let n in array.iterValues(node.childNodes))
263 // Evilness and such.
264 let (oldAttrs = attrs) {
265 attrs = (attr for each (attr in oldAttrs));
268 for (let attr in attrs || []) {
269 let [ns, localName] = DOM.parseNamespace(attr);
271 let val = attrs[attr];
273 savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]);
274 if (name === "highlight")
275 highlight.highlightNode(elem, val);
277 elem.setAttributeNS(ns || "", name, val);
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));
289 util.trapErrors("ready", obj, window);
291 function load(event) {
292 util.trapErrors("load", obj, window, event);
294 if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
295 util.trapErrors("visible", obj, window);
297 overlay.onWindowVisible.push(function () { obj.visible(window) });
301 if (doc.readyState === "complete")
304 window.addEventListener("load", util.wrapCallback(function onLoad(event) {
305 if (event.originalTarget === doc) {
306 window.removeEventListener("load", onLoad.wrapper, true);
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);
316 util.trapErrors("unload", obj, window, event);
319 util.trapErrors("cleanup", obj, window, "unload", event);
324 this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
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.
335 * @param {object} object The object to overlay.
336 * @param {object} overrides An object containing properties to
338 * @returns {function} A function which, when called, will remove
341 overlayObject: function (object, overrides) {
342 let original = Object.create(object);
343 overrides = update(Object.create(original), overrides);
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;
351 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
352 if (orig = Object.getOwnPropertyDescriptor(obj, k))
353 Object.defineProperty(original, k, orig);
356 if (orig = Object.getPropertyDescriptor(object, k))
357 Object.defineProperty(original, k, orig);
360 // Guard against horrible add-ons that use eval-based monkey
362 let value = desc.value;
363 if (callable(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);
372 let package_ = util.newURI(Components.stack.caller.filename).host;
373 util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
374 util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
380 Object.defineProperty(object, k, desc);
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 + "; $&");
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));
406 object[k] = original[k];
413 get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
415 get modules() this.windows.map(function (w) w.dactyl.modules),
418 * The most recently active dactyl window.
420 get activeWindow() this.windows[0],
422 set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
425 * A list of extant dactyl windows.
427 windows: Class.Memoize(function () [])
432 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
434 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: