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);
46 var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
47 init: function init() {
48 util.addObserver(this);
51 this.weakMap = WeakMap();
53 this.onWindowVisible = [];
56 id: Class.Memoize(function () config.addon.id),
59 * Adds an event listener for this session and removes it on
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
70 listen: function (target, event, callback, capture, allowUntrusted) {
71 let doc = target.ownerDocument || target.document || target;
72 let listeners = this.getData(doc, "listeners");
75 var [self, events] = [null, array.toObject([[event, callback]])];
77 [self, events] = [event, event[callback || "events"]];
79 for (let [event, callback] in Iterator(events)) {
80 let args = [util.weakReference(target),
82 util.wrapCallback(callback, self),
86 target.addEventListener.apply(target, args.slice(1));
92 * Remove an event listener.
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.
100 unlisten: function (target, event, callback, capture) {
101 let doc = target.ownerDocument || target.document || target;
102 let listeners = this.getData(doc, "listeners");
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));
111 return !args[0].get();
115 cleanup: function cleanup(reason) {
116 for (let doc in util.iterDocuments()) {
117 for (let elem in values(this.getData(doc, "overlayElements")))
119 elem.parentNode.removeChild(elem);
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);
125 for (let callback in values(this.getData(doc, "cleanup")))
126 util.trapErrors(callback, doc, reason);
128 this.unlisten(doc, true);
131 delete doc.defaultView[this.id];
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);
145 window.addEventListener("DOMContentLoaded", listener, true);
146 window.addEventListener("load", listener, true);
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;
157 getData: function getData(obj, key, constructor) {
159 if (!this.weakMap.has(obj))
161 this.weakMap.set(obj, {});
163 catch (e if e instanceof TypeError) {
164 // util.dump("Bad WeakMap key: " + obj + " " + Components.stack.caller);
167 if (!(id in obj && obj[id]))
173 data = data || this.weakMap.get(obj);
175 if (arguments.length == 1)
178 if (data[key] === undefined)
179 if (constructor === undefined || callable(constructor))
180 data[key] = (constructor || Array)();
182 data[key] = constructor;
187 setData: function setData(obj, key, val) {
188 let data = this.getData(obj);
190 return data[key] = val;
193 overlayWindow: function (url, fn) {
194 if (url instanceof Ci.nsIDOMWindow)
195 overlay._loadOverlay(url, fn);
197 Array.concat(url).forEach(function (url) {
198 if (!this.overlays[url])
199 this.overlays[url] = [];
200 this.overlays[url].push(fn);
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);
209 if (!this.onWindowVisible)
210 this.onWindowVisible = [];
211 this.observe(doc.defaultView, "toplevel-window-ready");
216 _loadOverlays: function _loadOverlays(window) {
217 let overlays = this.getData(window, "overlays");
219 for each (let obj in overlay.overlays[window.document.documentURI] || []) {
220 if (~overlays.indexOf(obj))
223 this._loadOverlay(window, obj(window));
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");
232 function insert(key, fn) {
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]))
240 for (let [elem, xml, attrs] in iterator) {
241 if (elem = doc.getElementById(String(elem))) {
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();
251 node = DOM.fromXML(xml, doc, obj.objects);
253 node = DOM.fromJSON(xml, doc, obj.objects, namespaces);
255 if (!(node instanceof Ci.nsIDOMDocumentFragment))
256 savedElems.push(node);
258 for (let n in array.iterValues(node.childNodes))
264 // Evilness and such.
265 let (oldAttrs = attrs) {
266 attrs = (attr for each (attr in oldAttrs));
269 for (let attr in attrs || []) {
270 let [ns, localName] = DOM.parseNamespace(attr);
272 let val = attrs[attr];
274 savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]);
275 if (name === "highlight")
276 highlight.highlightNode(elem, val);
278 elem.setAttributeNS(ns || "", name, val);
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));
290 util.trapErrors("ready", obj, window);
292 function load(event) {
293 util.trapErrors("load", obj, window, event);
295 if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
296 util.trapErrors("visible", obj, window);
298 overlay.onWindowVisible.push(function () { obj.visible(window) });
302 if (doc.readyState === "complete")
305 window.addEventListener("load", util.wrapCallback(function onLoad(event) {
306 if (event.originalTarget === doc) {
307 window.removeEventListener("load", onLoad.wrapper, true);
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);
317 util.trapErrors("unload", obj, window, event);
320 util.trapErrors("cleanup", obj, window, "unload", event);
325 this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
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.
336 * @param {object} object The object to overlay.
337 * @param {object} overrides An object containing properties to
339 * @returns {function} A function which, when called, will remove
342 overlayObject: function (object, overrides) {
343 let original = Object.create(object);
344 overrides = update(Object.create(original), overrides);
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;
352 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
353 if (orig = Object.getOwnPropertyDescriptor(obj, k))
354 Object.defineProperty(original, k, orig);
357 if (orig = Object.getPropertyDescriptor(object, k))
358 Object.defineProperty(original, k, orig);
361 // Guard against horrible add-ons that use eval-based monkey
363 let value = desc.value;
364 if (callable(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);
373 let package_ = util.newURI(Components.stack.caller.filename).host;
374 util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
375 util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
381 Object.defineProperty(object, k, desc);
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 + "; $&");
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));
407 object[k] = original[k];
414 get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
416 get modules() this.windows.map(function (w) w.dactyl.modules),
419 * The most recently active dactyl window.
421 get activeWindow() this.windows[0],
423 set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
426 * A list of extant dactyl windows.
428 windows: Class.Memoize(function () [])
433 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
435 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: