//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
try {
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("overlay", {
- exports: ["ModuleBase"],
- require: ["config", "io", "services", "util"]
+ exports: ["overlay"],
+ require: ["util"]
}, this);
-/**
- * @class ModuleBase
- * The base class for all modules.
- */
-var ModuleBase = Class("ModuleBase", {
- /**
- * @property {[string]} A list of module prerequisites which
- * must be initialized before this module is loaded.
- */
- requires: [],
+var getAttr = function getAttr(elem, ns, name)
+ elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
+var setAttr = function setAttr(elem, ns, name, val) {
+ if (val == null)
+ elem.removeAttributeNS(ns, name);
+ else
+ elem.setAttributeNS(ns, name, val);
+}
- toString: function () "[module " + this.constructor.className + "]"
-});
+var Overlay = Class("Overlay", {
+ init: function init(window) {
+ this.window = window;
+ },
-var Overlay = Module("Overlay", {
- init: function init() {
- services["dactyl:"]; // Hack. Force module initialization.
-
- config.loadStyles();
-
- util.overlayWindow(config.overlayChrome, function overlay(window) ({
- init: function onInit(document) {
- /**
- * @constructor Module
- *
- * Constructs a new ModuleBase class and makes arrangements for its
- * initialization. Arguments marked as optional must be either
- * entirely elided, or they must have the exact type specified.
- * Loading semantics are as follows:
- *
- * - A module is guaranteed not to be initialized before any of its
- * prerequisites as listed in its {@see ModuleBase#requires} member.
- * - A module is considered initialized once it's been instantiated,
- * its {@see Class#init} method has been called, and its
- * instance has been installed into the top-level {@see modules}
- * object.
- * - Once the module has been initialized, its module-dependent
- * initialization functions will be called as described hereafter.
- * @param {string} name The module's name as it will appear in the
- * top-level {@see modules} object.
- * @param {ModuleBase} base The base class for this module.
- * @optional
- * @param {Object} prototype The prototype for instances of this
- * object. The object itself is copied and not used as a prototype
- * directly.
- * @param {Object} classProperties The class properties for the new
- * module constructor.
- * @optional
- * @param {Object} moduleInit The module initialization functions
- * for the new module. Each function is called as soon as the named module
- * has been initialized, but after the module itself. The constructors are
- * guaranteed to be called in the same order that the dependent modules
- * were initialized.
- * @optional
- *
- * @returns {function} The constructor for the resulting module.
- */
- function Module(name) {
- let args = Array.slice(arguments);
-
- var base = ModuleBase;
- if (callable(args[1]))
- base = args.splice(1, 1)[0];
- let [, prototype, classProperties, moduleInit] = args;
- const module = Class(name, base, prototype, classProperties);
-
- module.INIT = moduleInit || {};
- module.modules = modules;
- module.prototype.INIT = module.INIT;
- module.requires = prototype.requires || [];
- Module.list.push(module);
- Module.constructors[name] = module;
- return module;
- }
- Module.list = [];
- Module.constructors = {};
+ cleanups: Class.Memoize(function () []),
+ objects: Class.Memoize(function () ({})),
- const BASE = "resource://dactyl-content/";
+ get doc() this.window.document,
- const create = window.Object.create || (function () {
- window.__dactyl_eval_string = "(function (proto) ({ __proto__: proto }))";
- JSMLoader.loadSubScript(BASE + "eval.js", window);
+ get win() this.window,
- let res = window.__dactyl_eval_result;
- delete window.__dactyl_eval_string;
- delete window.__dactyl_eval_result;
- return res;
- })();
+ $: function $(sel, node) DOM(sel, node || this.doc),
- const jsmodules = { NAME: "jsmodules" };
- const modules = update(create(jsmodules), {
- yes_i_know_i_should_not_report_errors_in_these_branches_thanks: [],
+ cleanup: function cleanup(window, reason) {
+ for (let fn in values(this.cleanups))
+ util.trapErrors(fn, this, window, reason);
+ }
+});
- jsmodules: jsmodules,
- get content() this.config.browser.contentWindow || window.content,
+var Overlay = Module("Overlay", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
+ init: function init() {
+ util.addObserver(this);
+ this.overlays = {};
- window: window,
+ this.onWindowVisible = [];
+ },
- Module: Module,
+ id: Class.Memoize(function () config.addon.id),
- load: function load(script) {
- for (let [i, base] in Iterator(prefix)) {
- try {
- JSMLoader.loadSubScript(base + script + ".js", modules, "UTF-8");
- return;
- }
- catch (e) {
- if (typeof e !== "string") {
- util.dump("Trying: " + (base + script + ".js") + ":");
- util.reportError(e);
- }
- }
- }
- try {
- require(jsmodules, script);
- }
- catch (e) {
- util.dump("Loading script " + script + ":");
- util.reportError(e);
+ /**
+ * Adds an event listener for this session and removes it on
+ * dactyl shutdown.
+ *
+ * @param {Element} target The element on which to listen.
+ * @param {string} event The event to listen for.
+ * @param {function} callback The function to call when the event is received.
+ * @param {boolean} capture When true, listen during the capture
+ * phase, otherwise during the bubbling phase.
+ * @param {boolean} allowUntrusted When true, allow capturing of
+ * untrusted events.
+ */
+ listen: function (target, event, callback, capture, allowUntrusted) {
+ let doc = target.ownerDocument || target.document || target;
+ let listeners = this.getData(doc, "listeners");
+
+ if (!isObject(event))
+ var [self, events] = [null, array.toObject([[event, callback]])];
+ else
+ [self, events] = [event, event[callback || "events"]];
+
+ for (let [event, callback] in Iterator(events)) {
+ let args = [util.weakReference(target),
+ event,
+ util.wrapCallback(callback, self),
+ capture,
+ allowUntrusted];
+
+ target.addEventListener.apply(target, args.slice(1));
+ listeners.push(args);
+ }
+ },
+
+ /**
+ * Remove an event listener.
+ *
+ * @param {Element} target The element on which to listen.
+ * @param {string} event The event to listen for.
+ * @param {function} callback The function to call when the event is received.
+ * @param {boolean} capture When true, listen during the capture
+ * phase, otherwise during the bubbling phase.
+ */
+ unlisten: function (target, event, callback, capture) {
+ let doc = target.ownerDocument || target.document || target;
+ let listeners = this.getData(doc, "listeners");
+ if (event === true)
+ target = null;
+
+ this.setData(doc, "listeners", listeners.filter(function (args) {
+ if (target == null || args[0].get() == target && args[1] == event && args[2].wrapped == callback && args[3] == capture) {
+ args[0].get().removeEventListener.apply(args[0].get(), args.slice(1));
+ return false;
+ }
+ return !args[0].get();
+ }));
+ },
+
+ cleanup: function cleanup(reason) {
+ for (let doc in util.iterDocuments()) {
+ for (let elem in values(this.getData(doc, "overlayElements")))
+ if (elem.parentNode)
+ elem.parentNode.removeChild(elem);
+
+ for (let [elem, ns, name, orig, value] in values(this.getData(doc, "overlayAttributes")))
+ if (getAttr(elem, ns, name) === value)
+ setAttr(elem, ns, name, orig);
+
+ for (let callback in values(this.getData(doc, "cleanup")))
+ util.trapErrors(callback, doc, reason);
+
+ this.unlisten(doc, true);
+
+ delete doc[this.id];
+ delete doc.defaultView[this.id];
+ }
+ },
+
+ observers: {
+ "toplevel-window-ready": function (window, data) {
+ let listener = util.wrapCallback(function listener(event) {
+ if (event.originalTarget === window.document) {
+ window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
+ window.removeEventListener("load", listener.wrapper, true);
+ overlay._loadOverlays(window);
+ }
+ });
+
+ window.addEventListener("DOMContentLoaded", listener, true);
+ window.addEventListener("load", listener, true);
+ },
+ "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
+ "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
+ "xul-window-visible": function () {
+ if (this.onWindowVisible)
+ this.onWindowVisible.forEach(function (f) f.call(this), this);
+ this.onWindowVisible = null;
+ }
+ },
+
+ getData: function getData(obj, key, constructor) {
+ let { id } = this;
+
+ if (!(id in obj && obj[id]))
+ obj[id] = {};
+
+ if (arguments.length == 1)
+ return obj[id];
+
+ if (obj[id][key] === undefined)
+ if (constructor === undefined || callable(constructor))
+ obj[id][key] = (constructor || Array)();
+ else
+ obj[id][key] = constructor;
+
+ return obj[id][key];
+ },
+
+ setData: function setData(obj, key, val) {
+ let { id } = this;
+
+ if (!(id in obj))
+ obj[id] = {};
+
+ return obj[id][key] = val;
+ },
+
+ overlayWindow: function (url, fn) {
+ if (url instanceof Ci.nsIDOMWindow)
+ overlay._loadOverlay(url, fn);
+ else {
+ Array.concat(url).forEach(function (url) {
+ if (!this.overlays[url])
+ this.overlays[url] = [];
+ this.overlays[url].push(fn);
+ }, this);
+
+ for (let doc in util.iterDocuments())
+ if (~["interactive", "complete"].indexOf(doc.readyState)) {
+ this.observe(doc.defaultView, "xul-window-visible");
+ this._loadOverlays(doc.defaultView);
+ }
+ else {
+ if (!this.onWindowVisible)
+ this.onWindowVisible = [];
+ this.observe(doc.defaultView, "toplevel-window-ready");
+ }
+ }
+ },
+
+ _loadOverlays: function _loadOverlays(window) {
+ let overlays = this.getData(window, "overlays");
+
+ for each (let obj in overlay.overlays[window.document.documentURI] || []) {
+ if (~overlays.indexOf(obj))
+ continue;
+ overlays.push(obj);
+ this._loadOverlay(window, obj(window));
+ }
+ },
+
+ _loadOverlay: function _loadOverlay(window, obj) {
+ let doc = window.document;
+ let elems = this.getData(doc, "overlayElements");
+ let attrs = this.getData(doc, "overlayAttributes");
+
+ function insert(key, fn) {
+ if (obj[key]) {
+ let iterator = Iterator(obj[key]);
+ if (!isObject(obj[key]))
+ iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
+
+ for (let [elem, xml, attr] in iterator) {
+ if (elem = doc.getElementById(elem)) {
+ let node = DOM.fromXML(xml, doc, obj.objects);
+ if (!(node instanceof Ci.nsIDOMDocumentFragment))
+ elems.push(node);
+ else
+ for (let n in array.iterValues(node.childNodes))
+ elems.push(n);
+
+ fn(elem, node);
+ for each (let attr in attr || []) {
+ let ns = attr.namespace(), name = attr.localName();
+ attrs.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
+ if (attr.name() != "highlight")
+ elem.setAttributeNS(ns, name, String(attr));
+ else
+ highlight.highlightNode(elem, String(attr));
}
- },
-
- newContext: function newContext(proto, normal) {
- if (normal)
- return create(proto);
- let sandbox = Components.utils.Sandbox(window, { sandboxPrototype: proto || modules, wantXrays: false });
- // Hack:
- sandbox.Object = jsmodules.Object;
- sandbox.Math = jsmodules.Math;
- sandbox.__proto__ = proto || modules;
- return sandbox;
- },
-
- get ownPropertyValues() array.compact(
- Object.getOwnPropertyNames(this)
- .map(function (name) Object.getOwnPropertyDescriptor(this, name).value, this)),
-
- get moduleList() this.ownPropertyValues.filter(function (mod) mod instanceof this.ModuleBase || mod.isLocalModule, this)
- });
- modules.plugins = create(modules);
- modules.modules = modules;
- window.dactyl = { modules: modules };
-
- let prefix = [BASE, "resource://dactyl-local-content/"];
-
- defineModule.time("load", null, function _load() {
- ["addons",
- "base",
- "io",
- "commands",
- "completion",
- "config",
- "contexts",
- "downloads",
- "finder",
- "highlight",
- "javascript",
- "messages",
- "options",
- "overlay",
- "prefs",
- "sanitizer",
- "services",
- "storage",
- "styles",
- "template",
- "util"
- ].forEach(function (name) defineModule.time("load", name, require, null, jsmodules, name));
-
- ["dactyl",
- "modes",
- "commandline",
- "abbreviations",
- "autocommands",
- "buffer",
- "editor",
- "events",
- "hints",
- "mappings",
- "marks",
- "mow",
- "statusline"
- ].forEach(function (name) defineModule.time("load", name, modules.load, modules, name));
- }, this);
- },
- load: function onLoad(document) {
- // This is getting to be horrible. --Kris
-
- var { modules, Module } = window.dactyl.modules;
- delete window.dactyl;
-
- const start = Date.now();
- const deferredInit = { load: {} };
- const seen = Set();
- const loaded = Set();
- modules.loaded = loaded;
-
- function load(module, prereq, frame) {
- if (isString(module)) {
- if (!Module.constructors.hasOwnProperty(module))
- modules.load(module);
- module = Module.constructors[module];
}
+ }
+ }
+ }
+
+ insert("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
+ insert("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
+ insert("append", function (elem, dom) elem.appendChild(dom));
+ insert("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
+ if (obj.ready)
+ util.trapErrors("ready", obj, window);
+
+ function load(event) {
+ util.trapErrors("load", obj, window, event);
+ if (obj.visible)
+ if (!event || !overlay.onWindowVisible || window != util.topWindow(window))
+ util.trapErrors("visible", obj, window);
+ else
+ overlay.onWindowVisible.push(function () { obj.visible(window) });
+ }
+
+ if (obj.load)
+ if (doc.readyState === "complete")
+ load();
+ else
+ window.addEventListener("load", util.wrapCallback(function onLoad(event) {
+ if (event.originalTarget === doc) {
+ window.removeEventListener("load", onLoad.wrapper, true);
+ load(event);
+ }
+ }), true);
- try {
- if (module.className in loaded)
- return;
- if (module.className in seen)
- throw Error("Module dependency loop.");
- Set.add(seen, module.className);
-
- for (let dep in values(module.requires))
- load(Module.constructors[dep], module.className);
+ if (obj.unload || obj.cleanup)
+ this.listen(window, "unload", function unload(event) {
+ if (event.originalTarget === doc) {
+ overlay.unlisten(window, "unload", unload);
+ if (obj.unload)
+ util.trapErrors("unload", obj, window, event);
- defineModule.loadLog.push("Load" + (isString(prereq) ? " " + prereq + " dependency: " : ": ") + module.className);
- if (frame && frame.filename)
- defineModule.loadLog.push(" from: " + util.fixURI(frame.filename) + ":" + frame.lineNumber);
+ if (obj.cleanup)
+ util.trapErrors("cleanup", obj, window, "unload", event);
+ }
+ });
- let obj = defineModule.time(module.className, "init", module);
- Class.replaceProperty(modules, module.className, obj);
- loaded[module.className] = true;
+ if (obj.cleanup)
+ this.getData(doc, "cleanup").push(bind("cleanup", obj, window));
+ },
- if (loaded.dactyl && obj.signals)
- modules.dactyl.registerObservers(obj);
+ /**
+ * Overlays an object with the given property overrides. Each
+ * property in *overrides* is added to *object*, replacing any
+ * original value. Functions in *overrides* are augmented with the
+ * new properties *super*, *supercall*, and *superapply*, in the
+ * same manner as class methods, so that they may call their
+ * overridden counterparts.
+ *
+ * @param {object} object The object to overlay.
+ * @param {object} overrides An object containing properties to
+ * override.
+ * @returns {function} A function which, when called, will remove
+ * the overlay.
+ */
+ overlayObject: function (object, overrides) {
+ let original = Object.create(object);
+ overrides = update(Object.create(original), overrides);
+
+ Object.getOwnPropertyNames(overrides).forEach(function (k) {
+ let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
+ if (desc.value instanceof Class.Property)
+ desc = desc.value.init(k) || desc.value;
+
+ if (k in object) {
+ for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
+ if (orig = Object.getOwnPropertyDescriptor(obj, k))
+ Object.defineProperty(original, k, orig);
+
+ if (!orig)
+ if (orig = Object.getPropertyDescriptor(object, k))
+ Object.defineProperty(original, k, orig);
+ }
- frob(module.className);
- }
- catch (e) {
- util.dump("Loading " + (module && module.className) + ":");
- util.reportError(e);
+ // Guard against horrible add-ons that use eval-based monkey
+ // patching.
+ let value = desc.value;
+ if (callable(desc.value)) {
+
+ delete desc.value;
+ delete desc.writable;
+ desc.get = function get() value;
+ desc.set = function set(val) {
+ if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
+ Class.replaceProperty(this, k, val);
+ else {
+ let package_ = util.newURI(Components.stack.caller.filename).host;
+ util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
+ util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
}
- return modules[module.className];
- }
-
- function deferInit(name, INIT, mod) {
- let init = deferredInit[name] = deferredInit[name] || {};
- let className = mod.className || mod.constructor.className;
+ };
+ }
- init[className] = function callee() {
- if (!callee.frobbed)
- defineModule.time(className, name, INIT[name], mod,
- modules.dactyl, modules, window);
- callee.frobbed = true;
- };
+ try {
+ Object.defineProperty(object, k, desc);
- INIT[name].require = function (name) { init[name](); };
+ if (callable(value)) {
+ var sentinel = "(function DactylOverlay() {}())"
+ value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
+ value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
}
-
- function frobModules() {
- Module.list.forEach(function frobModule(mod) {
- if (!mod.frobbed) {
- modules.__defineGetter__(mod.className, function () {
- delete modules[mod.className];
- return load(mod.className, null, Components.stack.caller);
- });
- Object.keys(mod.prototype.INIT)
- .forEach(function (name) { deferInit(name, mod.prototype.INIT, mod); });
- }
- mod.frobbed = true;
- });
+ }
+ catch (e) {
+ try {
+ if (value) {
+ object[k] = value;
+ return;
+ }
}
- defineModule.modules.forEach(function defModule(mod) {
- let names = Set(Object.keys(mod.INIT));
- if ("init" in mod.INIT)
- Set.add(names, "init");
-
- keys(names).forEach(function (name) { deferInit(name, mod.INIT, mod); });
- });
-
- function frob(name) { values(deferredInit[name] || {}).forEach(call); }
-
- frobModules();
- frob("init");
- modules.config.scripts.forEach(modules.load);
- frobModules();
+ catch (f) {}
+ util.reportError(e);
+ }
+ }, this);
- defineModule.modules.forEach(function defModule({ lazyInit, constructor: { className } }) {
- if (!lazyInit) {
- frob(className);
- Class.replaceProperty(modules, className, modules[className]);
+ return function unwrap() {
+ for each (let k in Object.getOwnPropertyNames(original))
+ if (Object.getOwnPropertyDescriptor(object, k).configurable)
+ Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
+ else {
+ try {
+ object[k] = original[k];
}
- else
- modules.__defineGetter__(className, function () {
- delete modules[className];
- frob(className);
- return modules[className] = modules[className];
- });
- });
+ catch (e) {}
+ }
+ };
+ },
- // Module.list.forEach(load);
- frob("load");
- modules.times = update({}, defineModule.times);
+ get activeModules() this.activeWindow && this.activeWindow.dactyl.modules,
- defineModule.loadLog.push("Loaded in " + (Date.now() - start) + "ms");
+ get modules() this.windows.map(function (w) w.dactyl.modules),
- modules.events.listen(window, "unload", function onUnload() {
- window.removeEventListener("unload", onUnload.wrapped, false);
+ /**
+ * The most recently active dactyl window.
+ */
+ get activeWindow() this.windows[0],
- for each (let mod in modules.moduleList.reverse()) {
- mod.stale = true;
+ set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)),
- if ("destroy" in mod)
- util.trapErrors("destroy", mod);
- }
- }, false);
- }
- }));
- }
+ /**
+ * A list of extant dactyl windows.
+ */
+ windows: Class.Memoize(function () [])
});
endModule();