1 // Copyright (c) 2009-2014 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(() => []),
31 objects: Class.Memoize(() => ({})),
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(() => 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 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));
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);
120 for (let elem in values(this.getData(doc, "overlayElements")))
122 elem.parentNode.removeChild(elem);
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);
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(f => { f.call(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);
189 if (val !== undefined)
190 return data[key] = val;
195 overlayWindow: function overlayWindow(url, fn) {
196 if (url instanceof Ci.nsIDOMWindow)
197 overlay._loadOverlay(url, fn);
199 Array.concat(url).forEach(function (url) {
200 if (!this.overlays[url])
201 this.overlays[url] = [];
202 this.overlays[url].push(fn);
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);
211 if (!this.onWindowVisible)
212 this.onWindowVisible = [];
213 this.observe(doc.defaultView, "toplevel-window-ready");
218 _loadOverlays: function _loadOverlays(window) {
219 let overlays = this.getData(window, "overlays");
221 for (let obj of overlay.overlays[window.document.documentURI] || []) {
222 if (~overlays.indexOf(obj))
225 this._loadOverlay(window, obj(window));
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");
234 function insert(key, fn) {
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]));
242 for (let [elem, xml, attrs] in iterator) {
243 if (elem = doc.getElementById(String(elem))) {
247 namespaces = iter([k.slice(6), DOM.fromJSON.namespaces[v] || v]
248 for ([k, v] in Iterator(attrs))
249 if (/^xmlns(?:$|:)/.test(k))).toObject();
251 let node = DOM.fromJSON(xml, doc, obj.objects, namespaces);
253 if (!(node instanceof Ci.nsIDOMDocumentFragment))
254 savedElems.push(node);
256 for (let n in array.iterValues(node.childNodes))
261 for (let attr in attrs || []) {
262 let [ns, localName] = DOM.parseNamespace(attr);
264 let val = attrs[attr];
266 savedAttrs.push([elem, ns, name, getAttr(elem, ns, name), val]);
267 if (name === "highlight")
268 highlight.highlightNode(elem, val);
270 elem.setAttributeNS(ns || "", name, val);
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));
282 util.trapErrors("ready", obj, window);
284 function load(event) {
285 util.trapErrors("load", obj, window, event);
287 if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
288 util.trapErrors("visible", obj, window);
290 overlay.onWindowVisible.push(function () { obj.visible(window) });
294 if (doc.readyState === "complete")
297 window.addEventListener("load", util.wrapCallback(function onLoad(event) {
298 if (event.originalTarget === doc) {
299 window.removeEventListener("load", onLoad.wrapper, true);
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);
309 util.trapErrors("unload", obj, window, event);
312 util.trapErrors("cleanup", obj, window, "unload", event);
317 this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
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.
328 * @param {object} object The object to overlay.
329 * @param {object} overrides An object containing properties to
331 * @returns {function} A function which, when called, will remove
334 overlayObject: function (object, overrides) {
335 let original = Object.create(object);
336 overrides = update(Object.create(original), overrides);
338 Object.getOwnPropertyNames(overrides).forEach(function (k) {
339 let desc = Object.getOwnPropertyDescriptor(overrides, k);
341 if (desc.value instanceof Class.Property)
342 desc = desc.value.init(k) || desc.value;
345 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj)) {
346 var orig = Object.getOwnPropertyDescriptor(obj, k);
348 Object.defineProperty(original, k, orig);
352 orig = Object.getPropertyDescriptor(object, k);
354 Object.defineProperty(original, k, orig);
358 // Guard against horrible add-ons that use eval-based monkey
360 let value = desc.value;
361 if (callable(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);
370 let package_ = util.newURI(Components.stack.caller.filename).host;
371 util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
372 util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
378 Object.defineProperty(object, k, desc);
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 + "; $&");
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));
404 object[k] = original[k];
411 get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
413 get modules() [w.dactyl.modules for (w of this.windows)],
416 * The most recently active dactyl window.
419 let win = this._activeWindow && this._activeWindow.get();
420 return this.windows.has(win) && win;
423 set activeWindow(win) this._activeWindow = util.weakReference(win),
426 * A list of extant dactyl windows.
428 windows: Class.Memoize(() => RealSet())
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 sts=4 ts=8 et ft=javascript: