diff options
author | Kris Maglione <maglione.k@gmail.com> | 2011-08-14 23:30:05 -0400 |
---|---|---|
committer | Kris Maglione <maglione.k@gmail.com> | 2011-08-14 23:30:05 -0400 |
commit | 681af3e618eed5d24c581c3ea5dee1907e404e7d (patch) | |
tree | 88f49f14cb4fc76484eec56e4684f444c98c5e15 /common | |
parent | 236a894c8913d8da6e2297dca1cacf363bca8a51 (diff) | |
download | pentadactyl-681af3e618eed5d24c581c3ea5dee1907e404e7d.tar.gz |
Move DOM and config properties from the util namespace to the DOM and config namespaces, respectively.
Diffstat (limited to 'common')
-rw-r--r-- | common/content/bookmarks.js | 8 | ||||
-rw-r--r-- | common/content/buffer.js | 60 | ||||
-rw-r--r-- | common/content/commandline.js | 16 | ||||
-rw-r--r-- | common/content/dactyl.js | 31 | ||||
-rw-r--r-- | common/content/editor.js | 15 | ||||
-rw-r--r-- | common/content/events.js | 29 | ||||
-rw-r--r-- | common/content/hints.js | 34 | ||||
-rw-r--r-- | common/content/marks.js | 6 | ||||
-rw-r--r-- | common/content/mow.js | 18 | ||||
-rw-r--r-- | common/content/statusline.js | 2 | ||||
-rw-r--r-- | common/content/tabs.js | 12 | ||||
-rw-r--r-- | common/modules/addons.jsm | 2 | ||||
-rw-r--r-- | common/modules/base.jsm | 4 | ||||
-rw-r--r-- | common/modules/config.jsm | 87 | ||||
-rw-r--r-- | common/modules/finder.jsm | 6 | ||||
-rw-r--r-- | common/modules/io.jsm | 36 | ||||
-rw-r--r-- | common/modules/overlay.jsm | 26 | ||||
-rw-r--r-- | common/modules/styles.jsm | 2 | ||||
-rw-r--r-- | common/modules/util.jsm | 919 |
19 files changed, 719 insertions, 594 deletions
diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index 388b8f20..97c2eaad 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -116,13 +116,13 @@ var Bookmarks = Module("bookmarks", { */ addSearchKeyword: function addSearchKeyword(elem) { if (elem instanceof HTMLFormElement || elem.form) - var [url, post, charset] = util.parseForm(elem); + var { url, postData, charset } = DOM(elem).formData; else - var [url, post, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet]; + var [url, postData, charset] = [elem.href || elem.src, null, elem.ownerDocument.characterSet]; let options = { "-title": "Search " + elem.ownerDocument.title }; - if (post != null) - options["-post"] = post; + if (postData != null) + options["-postData"] = postData; if (charset != null && charset !== "UTF-8") options["-charset"] = charset; diff --git a/common/content/buffer.js b/common/content/buffer.js index 3345a814..78dcd824 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -16,7 +16,6 @@ */ var Buffer = Module("buffer", { init: function init() { - this.evaluateXPath = util.evaluateXPath; this.pageInfo = {}; this.addPageInfoSection("e", "Search Engines", function (verbose) { @@ -24,8 +23,8 @@ var Buffer = Module("buffer", { let n = 1; let nEngines = 0; for (let { document: doc } in values(buffer.allFrames())) { - let engines = util.evaluateXPath(["link[@href and @rel='search' and @type='application/opensearchdescription+xml']"], doc); - nEngines += engines.snapshotLength; + let engines = DOM("link[href][rel=search][type='application/opensearchdescription+xml']", doc); + nEngines += engines.length; if (verbose) for (let link in engines) @@ -79,7 +78,7 @@ var Buffer = Module("buffer", { for (let [i, win] in Iterator(buffer.allFrames())) { let doc = win.document; - for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) { + for (let link in DOM("link[href][rel=feed], link[href][rel=alternate][type]", doc)) { let rel = link.rel.toLowerCase(); let feed = { title: link.title, href: link.href, type: link.type || "" }; if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) { @@ -612,7 +611,7 @@ var Buffer = Module("buffer", { }; DOM(elem).mousedown(params).mouseup(params); - if (!util.haveGecko("2b")) + if (!config.haveGecko("2b")) DOM(elem).click(params); let sel = util.selectionController(win); @@ -855,7 +854,7 @@ var Buffer = Module("buffer", { dactyl.assert(idx in elems); let elem = elems[idx][0]; - elem.scrollIntoView(true); + DOM(elem).scrollIntoView(); let sel = elem.ownerDocument.defaultView.getSelection(); sel.removeAllRanges(); @@ -897,7 +896,7 @@ var Buffer = Module("buffer", { // focus next frame and scroll into view dactyl.focus(frames[next]); if (frames[next] != content) - frames[next].frameElement.scrollIntoView(false); + DOM(frames[next].frameElement).scrollIntoView(); // add the frame indicator let doc = frames[next].document; @@ -1207,7 +1206,7 @@ var Buffer = Module("buffer", { selection.removeAllRanges(); selection.addRange(range); } - return util.domToString(range); + return DOM.stringify(range); }, getDefaultNames: function getDefaultNames(node) { @@ -1242,7 +1241,7 @@ var Buffer = Module("buffer", { names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")), "File Name"]); return names.filter(function ([leaf, title]) leaf) - .map(function ([leaf, title]) [leaf.replace(util.OS.illegalCharacters, encodeURIComponent) + .map(function ([leaf, title]) [leaf.replace(config.OS.illegalCharacters, encodeURIComponent) .replace(re, ext), title]); }, @@ -1256,7 +1255,7 @@ var Buffer = Module("buffer", { pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth", overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth"; - let style = util.computedStyle(elem); + let style = DOM(elem).style; let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2])); let realSize = elem[size]; // Stupid Gecko eccentricities. May fail for quirks mode documents. @@ -1288,7 +1287,7 @@ var Buffer = Module("buffer", { if (top != null) elem.scrollTop = top; - if (util.haveGecko("2.0") && !util.haveGecko("7.*")) + if (config.haveGecko("2.0") && !util.haveGecko("7.*")) elem.ownerDocument.defaultView .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils) .redraw(); @@ -1307,7 +1306,7 @@ var Buffer = Module("buffer", { * given direction. */ scrollHorizontal: function scrollHorizontal(elem, unit, number) { - let fontSize = parseInt(util.computedStyle(elem).fontSize); + let fontSize = parseInt(DOM(elem).style.fontSize); let increment; if (unit == "columns") increment = fontSize; // Good enough, I suppose. @@ -1337,7 +1336,7 @@ var Buffer = Module("buffer", { * given direction. */ scrollVertical: function scrollVertical(elem, unit, number) { - let fontSize = parseInt(util.computedStyle(elem).lineHeight); + let fontSize = parseInt(DOM(elem).style.lineHeight); let increment; if (unit == "lines") increment = fontSize; @@ -1385,7 +1384,7 @@ var Buffer = Module("buffer", { * column ordinal to scroll to. */ scrollToPosition: function scrollToPosition(elem, horizontal, vertical) { - let style = util.computedStyle(elem); + let style = DOM(elem).style; Buffer.scrollTo(elem, horizontal == null ? null : horizontal == 0 ? 0 : this._exWidth(elem) * horizontal, @@ -1399,7 +1398,7 @@ var Buffer = Module("buffer", { * @param {Element} elem The element to scroll. */ getScrollPosition: function getPosition(elem) { - let style = util.computedStyle(elem); + let style = DOM(elem).style; return { x: elem.scrollLeft && elem.scrollLeft / this._exWidth(elem), y: elem.scrollTop / parseFloat(style.lineHeight) @@ -1407,14 +1406,13 @@ var Buffer = Module("buffer", { }, _exWidth: function _exWidth(elem) { - let div = elem.appendChild( - util.xmlToDom(<elem style="width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;"/>, - elem.ownerDocument)); + let div = DOM(<elem style="width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;"/>, + elem.ownerDocument).appendTo(elem); try { - return parseFloat(util.computedStyle(div).width); + return parseFloat(div.style.width); } finally { - div.parentNode.removeChild(div); + div.remove(); } }, @@ -1443,7 +1441,7 @@ var Buffer = Module("buffer", { let arg = args[0]; // FIXME: arg handling is a bit of a mess, check for filename - dactyl.assert(!arg || arg[0] == ">" && !util.OS.isWindows, + dactyl.assert(!arg || arg[0] == ">" && !config.OS.isWindows, _("error.trailingCharacters")); const PRINTER = "PostScript/default"; @@ -1913,18 +1911,20 @@ var Buffer = Module("buffer", { let frames = buffer.allFrames(null, true); - let elements = array.flatten(frames.map(function (win) [m for (m in util.evaluateXPath(xpath, win.document))])) + let elements = array.flatten(frames.map(function (win) [m for (m in DOM.XPath(xpath, win.document))])) .filter(function (elem) { if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement])) return Editor.getEditor(elem.contentWindow); - if (elem.readOnly || elem instanceof HTMLInputElement && !Set.has(util.editableInputs, elem.type)) + elem = DOM(elem); + + if (elem[0].readOnly || !DOM(elem).isEditable) return false; - let computedStyle = util.computedStyle(elem); - let rect = elem.getBoundingClientRect(); - return computedStyle.visibility != "hidden" && computedStyle.display != "none" && - (elem instanceof Ci.nsIDOMXULTextBoxElement || computedStyle.MozUserFocus != "ignore") && + let style = elem.style; + let rect = elem.rect; + return elem.isVisible && + (elem[0] instanceof Ci.nsIDOMXULTextBoxElement || style.MozUserFocus != "ignore") && rect.width && rect.height; }); @@ -1932,7 +1932,7 @@ var Buffer = Module("buffer", { elem = elements[Math.constrain(args.count, 1, elements.length) - 1]; } buffer.focusElement(elem); - util.scrollIntoView(elem); + DOM(elem).scrollIntoView(); }, { count: true }); @@ -2086,10 +2086,10 @@ var Buffer = Module("buffer", { keepQuotes: true, setter: function (vals) { for (let [k, v] in Iterator(vals)) - vals[k] = update(new String(v), { matcher: util.compileMatcher(Option.splitList(v)) }); + vals[k] = update(new String(v), { matcher: DOM.compileMatcher(Option.splitList(v)) }); return vals; }, - validator: function (value) util.validateMatcher.call(this, value) + validator: function (value) DOM.validateMatcher.call(this, value) && Object.keys(value).every(function (v) v.length == 1) }); diff --git a/common/content/commandline.js b/common/content/commandline.js index ca5b00d6..0675153b 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -279,7 +279,7 @@ var CommandWidgets = Class("CommandWidgets", { // some host apps use "hostPrefixContext-copy" ids let xpath = "//xul:menuitem[contains(@id, '" + "ontext-" + tail + "') and not(starts-with(@id, 'dactyl-'))]"; document.getElementById("dactyl-context-" + tail).style.listStyleImage = - util.computedStyle(util.evaluateXPath(xpath, document).snapshotItem(0)).listStyleImage; + DOM(DOM.XPath(xpath, document).snapshotItem(0)).style.listStyleImage; }); return document.getElementById("dactyl-contextmenu"); }), @@ -899,7 +899,7 @@ var CommandLine = Module("commandline", { this.savingOutput = true; dactyl.trapErrors.apply(dactyl, [fn, self].concat(Array.slice(arguments, 2))); this.savingOutput = false; - return output.map(function (elem) elem instanceof Node ? util.domToString(elem) : elem) + return output.map(function (elem) elem instanceof Node ? DOM.stringify(elem) : elem) .join("\n"); } }, { @@ -1641,8 +1641,10 @@ var ItemList = Class("ItemList", { _init: function _init() { this._div = this._dom( <div class="ex-command-output" highlight="Normal" style="white-space: nowrap"> - <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div> - <div key="completions"/> + <div key="wrapper"> + <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div> + <div key="completions"/> + </div> <div highlight="Completions"> { template.map(util.range(0, options["maxitems"] * 2), function (i) @@ -1653,7 +1655,7 @@ var ItemList = Class("ItemList", { </div> </div>, this._divNodes); this._doc.body.replaceChild(this._div, this._doc.body.firstChild); - util.scrollIntoView(this._div, true); + DOM(this._divNodes.wrapper).scrollIntoView(true); this._items.contextList.forEach(function init_eachContext(context) { delete context.cache.nodes; @@ -1758,7 +1760,7 @@ var ItemList = Class("ItemList", { this._divNodes.noCompletions.style.display = haveCompletions ? "none" : "block"; - this._completionElements = util.evaluateXPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc); + this._completionElements = DOM.XPath("//xhtml:div[@dactyl:highlight='CompItem']", this._doc); return true; }, @@ -1828,7 +1830,7 @@ var ItemList = Class("ItemList", { if (index >= 0) { this._getCompletion(index).setAttribute("selected", "true"); if (this._container.height != 0) - util.scrollIntoView(this._getCompletion(index)); + DOM(this._getCompletion(index)).scrollIntoView(); } //if (index == 0) diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 8e0301d7..b9395f84 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -698,7 +698,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { }; // Find the tags in the document. let addTags = function addTags(file, doc) { - for (let elem in util.evaluateXPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc)) + for (let elem in DOM.XPath("//@tag|//dactyl:tags/text()|//dactyl:tag/text()", doc)) for (let tag in values((elem.value || elem.textContent).split(/\s+/))) tagMap[tag] = file; }; @@ -716,7 +716,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { tagMap["all"] = tagMap["all.xml"] = "all"; tagMap["versions"] = tagMap["versions.xml"] = "versions"; let files = findHelpFile("all").map(function (doc) - [f.value for (f in util.evaluateXPath("//dactyl:include/@href", doc))]); + [f.value for (f in DOM.XPath("//dactyl:include/@href", doc))]); // Scrape the tags from the rest of the help files. array.flatten(files).forEach(function (file) { @@ -1255,10 +1255,11 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { * Opens one or more URLs. Returns true when load was initiated, or * false on error. * - * @param {string|Array} urls A representation of the URLs to open. May be + * @param {string|object|Array} urls A representation of the URLs to open. May be * either a string, which will be passed to - * {@see Dactyl#parseURLs}, or an array in the same format as - * would be returned by the same. + * {@link Dactyl#parseURLs}, an array in the same format as + * would be returned by the same, or an object as returned by + * {@link DOM#formData}. * @param {object} params A set of parameters specifying how to open the * URLs. The following properties are recognized: * @@ -1312,21 +1313,23 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { return; let browser = config.tabbrowser; - function open(urls, where) { + function open(loc, where) { try { - let url = Array.concat(urls)[0]; - let postdata = Array.concat(urls)[1]; + if (isArray(loc)) + loc = { url: loc[0], postData: loc[1] }; + else if (isString(loc)) + loc = { url: loc }; // decide where to load the first url switch (where) { case dactyl.NEW_TAB: if (!dactyl.has("tabs")) - return open(urls, dactyl.NEW_WINDOW); + return open(loc, dactyl.NEW_WINDOW); return prefs.withContext(function () { prefs.set("browser.tabs.loadInBackground", true); - return browser.loadOneTab(url, null, null, postdata, background).linkedBrowser.contentDocument; + return browser.loadOneTab(loc.url, null, null, loc.postData, background).linkedBrowser.contentDocument; }); case dactyl.NEW_WINDOW: @@ -1335,7 +1338,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { browser = win.dactyl && win.dactyl.modules.config.tabbrowser || win.getBrowser(); // FALLTHROUGH case dactyl.CURRENT_TAB: - browser.loadURIWithFlags(url, flags, null, null, postdata); + browser.loadURIWithFlags(loc.url, flags, null, null, loc.postData); return browser.contentWindow; } } @@ -1380,7 +1383,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { return urls.map(function (url) { url = url.trim(); - if (/^(\.{0,2}|~)(\/|$)/.test(url) || util.OS.isWindows && /^[a-z]:/i.test(url)) { + if (/^(\.{0,2}|~)(\/|$)/.test(url) || config.OS.isWindows && /^[a-z]:/i.test(url)) { try { // Try to find a matching file. let file = io.File(url); @@ -1945,7 +1948,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { function () { dactyl.restart(); }, { argCount: "0" }); - function findToolbar(name) util.evaluateXPath( + function findToolbar(name) DOM.XPath( "//*[@toolbarname=" + util.escapeString(name, "'") + " or " + "@toolbarname=" + util.escapeString(name.trim(), "'") + "]", document).snapshotItem(0); @@ -2136,7 +2139,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.nsISupportsWeakReference, ModuleBase), { completion.toolbar = function toolbar(context) { context.title = ["Toolbar"]; context.keys = { text: function (item) item.getAttribute("toolbarname"), description: function () "" }; - context.completions = util.evaluateXPath("//*[@toolbarname]", document); + context.completions = DOM.XPath("//*[@toolbarname]", document); }; completion.window = function window(context) { diff --git a/common/content/editor.js b/common/content/editor.js index 1c50dac9..cc60c721 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -209,7 +209,7 @@ var Editor = Module("editor", { var editor_ = window.GetCurrentEditor ? GetCurrentEditor() : Editor.getEditor(document.commandDispatcher.focusedWindow); dactyl.assert(editor_); - text = Array.map(editor_.rootElement.childNodes, function (e) util.domToString(e, true)).join(""); + text = Array.map(editor_.rootElement.childNodes, function (e) DOM.stringify(e, true)).join(""); } let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || ""; @@ -348,16 +348,7 @@ var Editor = Module("editor", { elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow; dactyl.assert(elem); - try { - if (elem instanceof Element) - return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor; - return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession) - .getEditorForWindow(elem); - } - catch (e) { - return null; - } + return DOM(elem).editor; }, getController: function (cmd) { @@ -582,7 +573,7 @@ var Editor = Module("editor", { addMotionMap("d", "Delete motion", true, function (editor) { editor.cut(); }); addMotionMap("c", "Change motion", true, function (editor) { editor.cut(); }, modes.INSERT); - addMotionMap("y", "Yank motion", false, function (editor, range) { dactyl.clipboardWrite(util.domToString(range)) }); + addMotionMap("y", "Yank motion", false, function (editor, range) { dactyl.clipboardWrite(DOM.stringify(range)) }); let bind = function bind(names, description, action, params) mappings.add([modes.INPUT], names, description, diff --git a/common/content/events.js b/common/content/events.js index 1b3a3d80..6f3090df 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -495,12 +495,6 @@ var Events = Module("events", { this._activeMenubar = false; this.listen(window, this, "events"); - - util.windows = [window].concat(util.windows); - }, - - destroy: function destroy() { - util.windows = util.windows.filter(function (w) w != window); }, signals: { @@ -922,14 +916,14 @@ var Events = Module("events", { // https://bugzilla.mozilla.org/show_bug.cgi?id=432951 // --- // - // The following fixes are only activated if util.OS.isMacOSX. + // The following fixes are only activated if config.OS.isMacOSX. // Technically, they prevent mappings from <C-Esc> (and // <C-C-]> if your fancy keyboard permits such things<?>), but // these <C-control> mappings are probably pathological (<C-Esc> // certainly is on Windows), and so it is probably - // harmless to remove the util.OS.isMacOSX if desired. + // harmless to remove the config.OS.isMacOSX if desired. // - else if (util.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) { + else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) { if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug key = "Esc"; modifier = modifier.replace("C-", ""); @@ -1019,7 +1013,7 @@ var Events = Module("events", { ["key", key.toLowerCase()]); } - let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey"; + let accel = config.OS.isMacOSX ? "metaKey" : "ctrlKey"; let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" }) .filter(function ([k, v]) this & k, prefs.get("ui.key.chromeAccess")) @@ -1160,7 +1154,7 @@ var Events = Module("events", { let elem = event.originalTarget; if (elem == window) - util.windows = [window].concat(util.windows.filter(function (w) w != window)); + overlay.activeWindow = window; elem.dactylHadFocus = true; if (event.target instanceof Ci.nsIDOMXULTextBoxElement) @@ -1444,7 +1438,7 @@ var Events = Module("events", { let haveInput = modes.stack.some(function (m) m.main.input); if (elem instanceof HTMLTextAreaElement - || elem instanceof Element && util.computedStyle(elem).MozUserModify === "read-write" + || elem instanceof Element && DOM(elem).style.MozUserModify === "read-write" || elem == null && win && Editor.getEditor(win)) { if (modes.main == modes.VISUAL && elem.selectionEnd == elem.selectionStart) @@ -1525,7 +1519,7 @@ var Events = Module("events", { key === "<Esc>" || key === "<C-[>", isHidden: function isHidden(elem, aggressive) { - if (util.computedStyle(elem).visibility !== "visible") + if (DOM(elem).style.visibility !== "visible") return true; if (aggressive) @@ -1539,12 +1533,9 @@ var Events = Module("events", { }, isInputElement: function isInputElement(elem) { - return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) || - isinstance(elem, [HTMLEmbedElement, - HTMLObjectElement, HTMLSelectElement, - HTMLTextAreaElement, - Ci.nsIDOMXULTextBoxElement]) || - elem instanceof Window && Editor.getEditor(elem); + return DOM(elem).isEditable || + isinstance(elem, [HTMLEmbedElement, HTMLObjectElement, + HTMLSelectElement]) }, kill: function kill(event) { diff --git a/common/content/hints.js b/common/content/hints.js index 459ac88a..2c4b29ed 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -255,7 +255,7 @@ var HintSession = Class("HintSession", CommandMode, { getContainerOffsets: function _getContainerOffsets(doc) { let body = doc.body || doc.documentElement; // TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug. - let style = util.computedStyle(body); + let style = DOM(body).style; if (style && /^(absolute|fixed|relative)$/.test(style.position)) { let rect = body.getClientRects()[0]; @@ -298,7 +298,7 @@ var HintSession = Class("HintSession", CommandMode, { return false; if (!rect.width || !rect.height) - if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && util.computedStyle(elem).float != "none" && isVisible(elem))) + if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && DOM(elem).style.float != "none" && isVisible(elem))) return false; let computedStyle = doc.defaultView.getComputedStyle(elem, null); @@ -309,12 +309,11 @@ var HintSession = Class("HintSession", CommandMode, { let body = doc.body || doc.querySelector("body"); if (body) { - let fragment = util.xmlToDom(<div highlight="hints"/>, doc); - body.appendChild(fragment); - util.computedStyle(fragment).height; // Force application of binding. - let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment; + let fragment = DOM(<div highlight="hints"/>, doc).appendTo(body); + fragment.style.height; // Force application of binding. + let container = doc.getAnonymousElementByAttribute(fragment[0], "anonid", "hints") || fragment[0]; - let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none;"/>, doc); + let baseNode = DOM(<span highlight="Hint" style="display: none;"/>, doc)[0]; let mode = this.hintMode; let res = mode.matcher(doc); @@ -342,7 +341,7 @@ var HintSession = Class("HintSession", CommandMode, { else hint.text = elem.textContent.toLowerCase(); - hint.span = baseNodeAbsolute.cloneNode(false); + hint.span = baseNode.cloneNode(false); let leftPos = Math.max((rect.left + offsetX), offsetX); let topPos = Math.max((rect.top + offsetY), offsetY); @@ -532,7 +531,7 @@ var HintSession = Class("HintSession", CommandMode, { // Goddamn stupid fucking Gecko 1.x security manager bullshit. try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; } - for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc)) + for (let elem in DOM.XPath("//*[@dactyl:highlight='hints']", doc)) elem.parentNode.removeChild(elem); for (let i in util.range(start, end + 1)) { this.pageHints[i].ambiguous = false; @@ -824,7 +823,7 @@ var Hints = Module("hints", { let type = elem.type; - if (elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type)) + if (DOM(elem).isInput) return [elem.value, false]; else { for (let [, option] in Iterator(options["hintinputs"])) { @@ -1080,7 +1079,7 @@ var Hints = Module("hints", { isVisible: function isVisible(elem, offScreen) { let rect = elem.getBoundingClientRect(); if (!rect.width || !rect.height) - if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && util.computedStyle(elem).float != "none" && isVisible(elem))) + if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && DOM(elem).style.float != "none" && isVisible(elem))) return false; let win = elem.ownerDocument.defaultView; @@ -1089,8 +1088,7 @@ var Hints = Module("hints", { rect.right + win.scrollX > win.scrolMaxX + win.innerWidth)) return false; - let computedStyle = util.computedStyle(elem, null); - if (computedStyle.visibility != "visible" || computedStyle.display == "none") + if (!DOM(elem).isVisible) return false; return true; }, @@ -1249,8 +1247,6 @@ var Hints = Module("hints", { function ({ self }) { self.escapeNumbers = !self.escapeNumbers; }); }, options: function () { - function xpath(arg) util.makeXPath(arg); - options.add(["extendedhinttags", "eht"], "XPath or CSS selector strings of hintable elements for extended hint modes", "regexpmap", { @@ -1267,10 +1263,10 @@ var Hints = Module("hints", { res ? res.matcher : default_, setter: function (vals) { for (let value in values(vals)) - value.matcher = util.compileMatcher(Option.splitList(value.result)); + value.matcher = DOM.compileMatcher(Option.splitList(value.result)); return vals; }, - validator: util.validateMatcher + validator: DOM.validateMatcher }); options.add(["hinttags", "ht"], @@ -1280,10 +1276,10 @@ var Hints = Module("hints", { "[tabindex],[role=link],[role=button],[contenteditable=true]", { setter: function (values) { - this.matcher = util.compileMatcher(values); + this.matcher = DOM.compileMatcher(values); return values; }, - validator: util.validateMatcher + validator: DOM.validateMatcher }); options.add(["hintkeys", "hk"], diff --git a/common/content/marks.js b/common/content/marks.js index e6bbb9b4..13355261 100644 --- a/common/content/marks.js +++ b/common/content/marks.js @@ -42,7 +42,7 @@ var Marks = Module("marks", { params.location = doc.documentURI, params.offset = buffer.scrollPosition; - params.path = util.generateXPath(buffer.findScrollable(0, params.offset.x)); + params.path = DOM(buffer.findScrollable(0, params.offset.x)).xpath; params.timestamp = Date.now() * 1000; params.equals = function (m) this.location == m.location && this.offset.x == m.offset.x @@ -227,11 +227,11 @@ var Marks = Module("marks", { if (!mark.xpath) var node = buffer.findScrollable(0, (mark.offset || mark.position).x) else - for (node in util.evaluateXPath(mark.xpath, buffer.focusedFrame.document)) + for (node in DOM.XPath(mark.xpath, buffer.focusedFrame.document)) break; util.assert(node); - util.scrollIntoView(node); + DOM(node).scrollIntoView(); if (mark.offset) Buffer.scrollToPosition(node, mark.offset.x, mark.offset.y); diff --git a/common/content/mow.js b/common/content/mow.js index bd055746..4254cb9c 100644 --- a/common/content/mow.js +++ b/common/content/mow.js @@ -24,7 +24,7 @@ var MOW = Module("mow", { if (options["more"] && this.isScrollable(1)) { // start the last executed command's output at the top of the screen let elements = this.document.getElementsByClassName("ex-command-output"); - elements[elements.length - 1].scrollIntoView(true); + DOM(elements[elements.length - 1]).scrollIntoView(true); } else this.body.scrollTop = this.body.scrollHeight; @@ -37,7 +37,7 @@ var MOW = Module("mow", { events.listen(window, this, "windowEvents"); modules.mow = this; - let fontSize = util.computedStyle(document.documentElement).fontSize; + let fontSize = DOM(document.documentElement).style.fontSize; styles.system.add("font-size", "dactyl://content/buffer.xhtml", "body { font-size: " + fontSize + "; } \ html|html > xul|scrollbar { visibility: collapse !important; }", @@ -94,7 +94,7 @@ var MOW = Module("mow", { * @param {string} highlightGroup */ echo: function echo(data, highlightGroup, silent) { - let body = this.document.body; + let body = DOM(this.document.body); this.widgets.message = null; if (!commandline.commandVisible) @@ -122,11 +122,11 @@ var MOW = Module("mow", { if (isObject(data) && !isinstance(data, _)) { this.lastOutput = null; - var output = util.xmlToDom(<div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}/>, - this.document); + var output = DOM(<div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}/>, + this.document); data.document = this.document; try { - output.appendChild(data.message); + output.append(data.message); } catch (e) { util.reportError(e); @@ -138,17 +138,17 @@ var MOW = Module("mow", { let style = isString(data) ? "pre-wrap" : "nowrap"; this.lastOutput = <div class="ex-command-output" style={"white-space: " + style} highlight={highlightGroup}>{data}</div>; - var output = util.xmlToDom(this.lastOutput, this.document); + var output = DOM(this.lastOutput, this.document); } // FIXME: need to make sure an open MOW is closed when commands // that don't generate output are executed if (!this.visible) { this.body.scrollTop = 0; - body.textContent = ""; + body.empty(); } - body.appendChild(output); + body.append(output); let str = typeof data !== "xml" && data.message || data; if (!silent) diff --git a/common/content/statusline.js b/common/content/statusline.js index b5dbee70..2d93e512 100644 --- a/common/content/statusline.js +++ b/common/content/statusline.js @@ -42,7 +42,7 @@ var StatusLine = Module("statusline", { color: inherit !important; } AddonButton:not(:hover) background: transparent; - ]]>)({ padding: util.OS.isMacOSX ? "padding-right: 10px !important;" : "" })); + ]]>)({ padding: config.OS.isMacOSX ? "padding-right: 10px !important;" : "" })); if (document.getElementById("appmenu-button")) highlight.loadCSS(<![CDATA[ diff --git a/common/content/tabs.js b/common/content/tabs.js index 3d9e6c7f..c5c156c0 100644 --- a/common/content/tabs.js +++ b/common/content/tabs.js @@ -37,7 +37,7 @@ var Tabs = Module("tabs", { this.tabBinding = styles.system.add("tab-binding", "chrome://browser/content/browser.xul", String.replace(<><![CDATA[ xul|tab { -moz-binding: url(chrome://dactyl/content/bindings.xml#tab) !important; } - ]]></>, /tab-./g, function (m) util.OS.isMacOSX ? "tab-mac" : m), + ]]></>, /tab-./g, function (m) config.OS.isMacOSX ? "tab-mac" : m), false, true); this.timeout(function () { @@ -75,14 +75,12 @@ var Tabs = Module("tabs", { if (!node("dactyl-tab-number")) { let img = node("tab-icon-image"); if (img) { - let nodes = {}; - let dom = util.xmlToDom(<xul xmlns:xul={XUL} xmlns:html={XHTML} + let dom = DOM(<xul xmlns:xul={XUL} xmlns:html={XHTML} ><xul:hbox highlight="tab-number"><xul:label key="icon" align="center" highlight="TabIconNumber" class="dactyl-tab-icon-number"/></xul:hbox ><xul:hbox highlight="tab-number"><html:div key="label" highlight="TabNumber" class="dactyl-tab-number"/></xul:hbox - ></xul>.*, document, nodes); - img.parentNode.appendChild(dom); - tab.__defineGetter__("dactylOrdinal", function () Number(nodes.icon.value)); - tab.__defineSetter__("dactylOrdinal", function (i) nodes.icon.value = nodes.label.textContent = i); + ></xul>.*, document).appendTo(img.parentNode); + tab.__defineGetter__("dactylOrdinal", function () Number(dom.nodes.icon.value)); + tab.__defineSetter__("dactylOrdinal", function (i) dom.nodes.icon.value = dom.nodes.label.textContent = i); } } } diff --git a/common/modules/addons.jsm b/common/modules/addons.jsm index 6f43950c..160b3178 100644 --- a/common/modules/addons.jsm +++ b/common/modules/addons.jsm @@ -111,7 +111,7 @@ var actions = { name: "extr[ehash]", description: "Reload an extension", action: function (addon) { - util.assert(util.haveGecko("2b"), _("command.notUseful", config.host)); + util.assert(config.haveGecko("2b"), _("command.notUseful", config.host)); util.timeout(function () { addon.userDisabled = true; addon.userDisabled = false; diff --git a/common/modules/base.jsm b/common/modules/base.jsm index 0c8c82f4..8cde5ce2 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -144,6 +144,7 @@ function defineModule(name, params, module) { use[mod] = use[mod] || []; use[mod].push(module); } + module._lastModule = currentModule; currentModule = module; module.startTime = Date.now(); } @@ -190,6 +191,7 @@ function endModule() { require(mod, currentModule.NAME, "use"); loaded[currentModule.NAME] = 1; + currentModule = currentModule._lastModule; } function require(obj, name, from) { @@ -732,7 +734,7 @@ function Class() { if (callable(args[0])) superclass = args.shift(); - if (loaded.util && util.haveGecko("6.0a1")) // Bug 657418. + if (loaded.config && config.haveGecko("6.0a1")) // Bug 657418. var Constructor = function Constructor() { var self = Object.create(Constructor.prototype, { constructor: { value: Constructor }, diff --git a/common/modules/config.jsm b/common/modules/config.jsm index 4da5a88d..2623b84b 100644 --- a/common/modules/config.jsm +++ b/common/modules/config.jsm @@ -23,7 +23,7 @@ var ConfigBase = Class("ConfigBase", { */ init: function init() { this.features.push = deprecated("Set.add", function push(feature) Set.add(this, feature)); - if (util.haveGecko("2b")) + if (this.haveGecko("2b")) Set.add(this.features, "Gecko2"); this.timeout(function () { @@ -40,7 +40,7 @@ var ConfigBase = Class("ConfigBase", { highlight.loadCSS(this.CSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1))); highlight.loadCSS(this.helpCSS.replace(/__MSG_(.*?)__/g, function (m0, m1) _(m1))); - if (!util.haveGecko("2b")) + if (!this.haveGecko("2b")) highlight.loadCSS(<![CDATA[ !TabNumber font-weight: bold; margin: 0px; padding-right: .8ex; !TabIconNumber { @@ -60,12 +60,12 @@ var ConfigBase = Class("ConfigBase", { let elem = services.appShell.hiddenDOMWindow.document.createElement("div"); elem.style.cssText = this.cssText; - let style = util.computedStyle(elem); let keys = iter(Styles.propertyIter(this.cssText)).map(function (p) p.name).toArray(); let bg = keys.some(function (k) /^background/.test(k)); let fg = keys.indexOf("color") >= 0; + let style = DOM(elem).style; prefs[bg ? "safeSet" : "safeReset"]("ui.textHighlightBackground", hex(style.backgroundColor)); prefs[fg ? "safeSet" : "safeReset"]("ui.textHighlightForeground", hex(style.color)); }; @@ -149,6 +149,87 @@ var ConfigBase = Class("ConfigBase", { }, /** + * A list of all known registered chrome and resource packages. + */ + get chromePackages() { + // Horrible hack. + let res = {}; + function process(manifest) { + for each (let line in manifest.split(/\n+/)) { + let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line); + if (match) + res[match[2]] = true; + } + } + function processJar(file) { + let jar = services.ZipReader(file); + if (jar) { + if (jar.hasEntry("chrome.manifest")) + process(File.readStream(jar.getInputStream("chrome.manifest"))); + jar.close(); + } + } + + for each (let dir in ["UChrm", "AChrom"]) { + dir = File(services.directory.get(dir, Ci.nsIFile)); + if (dir.exists() && dir.isDirectory()) + for (let file in dir.iterDirectory()) + if (/\.manifest$/.test(file.leafName)) + process(file.read()); + + dir = File(dir.parent); + if (dir.exists() && dir.isDirectory()) + for (let file in dir.iterDirectory()) + if (/\.jar$/.test(file.leafName)) + processJar(file); + + dir = dir.child("extensions"); + if (dir.exists() && dir.isDirectory()) + for (let ext in dir.iterDirectory()) { + if (/\.xpi$/.test(ext.leafName)) + processJar(ext); + else { + if (ext.isFile()) + ext = File(ext.read().replace(/\n*$/, "")); + let mf = ext.child("chrome.manifest"); + if (mf.exists()) + process(mf.read()); + } + } + } + return Object.keys(res).sort(); + }, + + /** + * Returns true if the current Gecko runtime is of the given version + * or greater. + * + * @param {string} ver The required version. + * @returns {boolean} + */ + haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0, + + /** Dactyl's notion of the current operating system platform. */ + OS: memoize({ + _arch: services.runtime.OS, + /** + * @property {string} The normalised name of the OS. This is one of + * "Windows", "Mac OS X" or "Unix". + */ + get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix", + /** @property {boolean} True if the OS is Windows. */ + get isWindows() this._arch == "WINNT", + /** @property {boolean} True if the OS is Mac OS X. */ + get isMacOSX() this._arch == "Darwin", + /** @property {boolean} True if the OS is some other *nix variant. */ + get isUnix() !this.isWindows && !this.isMacOSX, + /** @property {RegExp} A RegExp which matches illegal characters in path components. */ + get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g, + + get pathListSep() this.isWindows ? ";" : ":" + }), + + /** * @property {string} The pathname of the VCS repository clone's root * directory if the application is running from one via an extension * proxy file. diff --git a/common/modules/finder.jsm b/common/modules/finder.jsm index 6ff2caee..94f9e48a 100644 --- a/common/modules/finder.jsm +++ b/common/modules/finder.jsm @@ -403,8 +403,8 @@ var RangeFind = Class("RangeFind", { focus: function focus() { if (this.lastRange) - var node = util.evaluateXPath(RangeFind.selectNodePath, - this.lastRange.commonAncestorContainer).snapshotItem(0); + var node = DOM.XPath(RangeFind.selectNodePath, + this.lastRange.commonAncestorContainer).snapshotItem(0); if (node) { node.focus(); // Re-highlight collapsed selection @@ -517,7 +517,7 @@ var RangeFind = Class("RangeFind", { for (let frame in array.iterValues(win.frames)) { let range = doc.createRange(); - if (util.computedStyle(frame.frameElement).visibility == "visible") { + if (DOM(frame.frameElement).style.visibility == "visible") { range.selectNode(frame.frameElement); pushRange(pageStart, RangeFind.endpoint(range, true)); pageStart = RangeFind.endpoint(range, false); diff --git a/common/modules/io.jsm b/common/modules/io.jsm index 59eb8d98..aeead9b3 100644 --- a/common/modules/io.jsm +++ b/common/modules/io.jsm @@ -299,7 +299,7 @@ var IO = Module("io", { let rcFile1 = dir.child("." + config.name + "rc"); let rcFile2 = dir.child("_" + config.name + "rc"); - if (util.OS.isWindows) + if (config.OS.isWindows) [rcFile1, rcFile2] = [rcFile2, rcFile1]; if (rcFile1.exists() && rcFile1.isFile()) @@ -393,9 +393,9 @@ var IO = Module("io", { if (bin instanceof File || File.isAbsolutePath(bin)) return this.File(bin); - let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":"); + let dirs = services.environment.get("PATH").split(config.OS.isWindows ? ";" : ":"); // Windows tries the CWD first TODO: desirable? - if (util.OS.isWindows) + if (config.OS.isWindows) dirs = [io.cwd].concat(dirs); for (let [, dir] in Iterator(dirs)) @@ -408,7 +408,7 @@ var IO = Module("io", { // TODO: couldn't we just palm this off to the start command? // automatically try to add the executable path extensions on windows - if (util.OS.isWindows) { + if (config.OS.isWindows) { let extensions = services.environment.get("PATHEXT").split(";"); for (let [, extension] in Iterator(extensions)) { file = dir.child(bin + extension); @@ -478,7 +478,7 @@ var IO = Module("io", { system: function (command, input, callback) { util.dactyl.echomsg(_("io.callingShell", command), 4); - function escape(str) '"' + String.replace(str, /[\\"$]/g, "\\$&") + '"'; + let { shellEscape } = util.closure; return this.withTempFiles(function (stdin, stdout, cmd) { if (input instanceof File) @@ -505,17 +505,17 @@ var IO = Module("io", { util.assert(shell, _("error.invalid", "'shell'")); if (isArray(command)) - command = command.map(escape).join(" "); + command = command.map(shellEscape).join(" "); // TODO: implement 'shellredir' - if (util.OS.isWindows && !/sh/.test(shell.leafName)) { + if (config.OS.isWindows && !/sh/.test(shell.leafName)) { command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path; var res = this.run(shell, shcf.split(/\s+/).concat(command), callback ? async : true); } else { - cmd.write("cd " + escape(this.cwd.path) + "\n" + - ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path), - escape(shell.path), shcf, escape(command)].join(" ")); + cmd.write("cd " + shellEscape(this.cwd.path) + "\n" + + ["exec", ">" + shellEscape(stdout.path), "2>&1", "<" + shellEscape(stdin.path), + shellEscape(shell.path), shcf, shellEscape(command)].join(" ")); res = this.run("/bin/sh", ["-e", cmd.path], callback ? async : true); } @@ -556,7 +556,7 @@ var IO = Module("io", { const rtpvar = config.idName + "_RUNTIME"; let rtp = services.environment.get(rtpvar); if (!rtp) { - rtp = "~/" + (util.OS.isWindows ? "" : ".") + config.name; + rtp = "~/" + (config.OS.isWindows ? "" : ".") + config.name; services.environment.set(rtpvar, rtp); } return rtp; @@ -644,7 +644,7 @@ var IO = Module("io", { commands.add(["mks[yntax]"], "Generate a Vim syntax file", function (args) { - let runtime = util.OS.isWindows ? "~/vimfiles/" : "~/.vim/"; + let runtime = config.OS.isWindows ? "~/vimfiles/" : "~/.vim/"; let file = io.File(runtime + "syntax/" + config.name + ".vim"); if (args.length) file = io.File(args[0]); @@ -886,7 +886,7 @@ unlet s:cpo_save completion.environment = function environment(context) { context.title = ["Environment Variable", "Value"]; context.generate = function () - io.system(util.OS.isWindows ? "set" : "env") + io.system(config.OS.isWindows ? "set" : "env") .output.split("\n") .filter(function (line) line.indexOf("=") > 0) .map(function (line) line.match(/([^=]+)=(.*)/).slice(1)); @@ -956,7 +956,7 @@ unlet s:cpo_save completion.shellCommand = function shellCommand(context) { context.title = ["Shell Command", "Path"]; context.generate = function () { - let dirNames = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":"); + let dirNames = services.environment.get("PATH").split(config.OS.pathListSep); let commands = []; for (let [, dirName] in Iterator(dirNames)) { @@ -987,7 +987,7 @@ unlet s:cpo_save if (!match.path) { context.key = match.proto; context.advance(match.proto.length); - context.generate = function () util.chromePackages.map(function (p) [p, match.proto + p + "/"]); + context.generate = function () config.chromePackages.map(function (p) [p, match.proto + p + "/"]); } else if (match.scheme === "chrome") { context.key = match.prefix; @@ -1001,7 +1001,7 @@ unlet s:cpo_save } if (!match || match.scheme === "resource" && match.path) if (/^(\.{0,2}|~)\/|^file:/.test(context.filter) - || util.OS.isWindows && /^[a-z]:/i.test(context.filter) + || config.OS.isWindows && /^[a-z]:/i.test(context.filter) || util.getFile(context.filter) || io.isJarURL(context.filter)) completion.file(context, full); @@ -1030,7 +1030,7 @@ unlet s:cpo_save const { completion, options } = modules; var shell, shellcmdflag; - if (util.OS.isWindows) { + if (config.OS.isWindows) { shell = "cmd.exe"; shellcmdflag = "/c"; } @@ -1077,7 +1077,7 @@ unlet s:cpo_save "string", shellcmdflag, { getter: function (value) { - if (this.hasChanged || !util.OS.isWindows) + if (this.hasChanged || !config.OS.isWindows) return value; return /sh/.test(options["shell"]) ? "-c" : "/c"; } diff --git a/common/modules/overlay.jsm b/common/modules/overlay.jsm index 08c2829d..ec329f05 100644 --- a/common/modules/overlay.jsm +++ b/common/modules/overlay.jsm @@ -8,7 +8,7 @@ try { Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("overlay", { - exports: ["ModuleBase"], + exports: ["ModuleBase", "overlay"], require: ["config", "io", "services", "util"] }, this); @@ -28,11 +28,13 @@ var ModuleBase = Class("ModuleBase", { var Overlay = Module("Overlay", { init: function init() { + let overlay = this; + services["dactyl:"]; // Hack. Force module initialization. config.loadStyles(); - util.overlayWindow(config.overlayChrome, function overlay(window) ({ + util.overlayWindow(config.overlayChrome, function _overlay(window) ({ init: function onInit(document) { /** * @constructor Module @@ -308,9 +310,15 @@ var Overlay = Module("Overlay", { defineModule.loadLog.push("Loaded in " + (Date.now() - start) + "ms"); + util.dump(overlay); + + overlay.windows = array.uniq(overlay.windows.concat(window), true); + modules.events.listen(window, "unload", function onUnload() { window.removeEventListener("unload", onUnload.wrapped, false); + overlay.windows = overlay.windows.filter(function (w) w != window); + for each (let mod in modules.moduleList.reverse()) { mod.stale = true; @@ -320,7 +328,19 @@ var Overlay = Module("Overlay", { }, false); } })); - } + }, + + /** + * The most recently active dactyl window. + */ + get activeWindow() this.windows[0], + + set activeWindow(win) this.windows = [win].concat(this.windows.filter(function (w) w != win)), + + /** + * A list of extant dactyl windows. + */ + windows: Class.memoize(function () []) }); endModule(); diff --git a/common/modules/styles.jsm b/common/modules/styles.jsm index 65c32ec2..d164c92e 100644 --- a/common/modules/styles.jsm +++ b/common/modules/styles.jsm @@ -702,7 +702,7 @@ var Styles = Module("Styles", { })); }, completion: function (dactyl, modules, window) { - const names = Array.slice(util.computedStyle(window.document.createElement("div"))); + const names = Array.slice(DOM(<div/>, window.document).style); modules.completion.css = function (context) { context.title = ["CSS Property"]; context.keys = { text: function (p) p + ":", description: function () "" }; diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 59ba36bd..eeae6adf 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -13,7 +13,7 @@ Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("util", { exports: ["$", "DOM", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"], require: ["services"], - use: ["commands", "config", "highlight", "messages", "storage", "template"] + use: ["commands", "config", "highlight", "messages", "overlay", "storage", "template"] }, this); var XBL = Namespace("xbl", "http://www.mozilla.org/xbl"); @@ -85,7 +85,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), } }, - get activeWindow() this.windows[0], + activeWindow: deprecated("overlay.activeWindow", { get: function activeWindow() overlay.activeWindow }), dactyl: update(function dactyl(obj) { if (obj) @@ -93,7 +93,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return { __noSuchMethod__: function (meth, args) { - let win = util.activeWindow; + let win = overlay.activeWindow; var dactyl = global && global.dactyl || win && win.dactyl; if (!dactyl) @@ -204,55 +204,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return RegExp("[" + util.regexp.escape(list) + "]"); }, - get chromePackages() { - // Horrible hack. - let res = {}; - function process(manifest) { - for each (let line in manifest.split(/\n+/)) { - let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line); - if (match) - res[match[2]] = true; - } - } - function processJar(file) { - let jar = services.ZipReader(file); - if (jar) { - if (jar.hasEntry("chrome.manifest")) - process(File.readStream(jar.getInputStream("chrome.manifest"))); - jar.close(); - } - } - - for each (let dir in ["UChrm", "AChrom"]) { - dir = File(services.directory.get(dir, Ci.nsIFile)); - if (dir.exists() && dir.isDirectory()) - for (let file in dir.iterDirectory()) - if (/\.manifest$/.test(file.leafName)) - process(file.read()); - - dir = File(dir.parent); - if (dir.exists() && dir.isDirectory()) - for (let file in dir.iterDirectory()) - if (/\.jar$/.test(file.leafName)) - processJar(file); - - dir = dir.child("extensions"); - if (dir.exists() && dir.isDirectory()) - for (let ext in dir.iterDirectory()) { - if (/\.xpi$/.test(ext.leafName)) - processJar(ext); - else { - if (ext.isFile()) - ext = File(ext.read().replace(/\n*$/, "")); - let mf = ext.child("chrome.manifest"); - if (mf.exists()) - process(mf.read()); - } - } - } - return Object.keys(res).sort(); - }, - /** * Returns a shallow copy of *obj*. * @@ -451,79 +402,24 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return stack.top; }, - /** - * Compiles a CSS spec and XPath pattern matcher based on the given - * list. List elements prefixed with "xpath:" are parsed as XPath - * patterns, while other elements are parsed as CSS specs. The - * returned function will, given a node, return an iterator of all - * descendants of that node which match the given specs. - * - * @param {[string]} list The list of patterns to match. - * @returns {function(Node)} - */ - compileMatcher: function compileMatcher(list) { - let xpath = [], css = []; - for (let elem in values(list)) - if (/^xpath:/.test(elem)) - xpath.push(elem.substr(6)); - else - css.push(elem); + compileMatcher: deprecated("DOM.compileMatcher", { get: function compileMatcher() DOM.compileMatcher }), + computedStyle: deprecated("DOM#style", function computedStyle(elem) DOM(elem).style), + domToString: deprecated("DOM.stringify", { get: function domToString() DOM.stringify }), + editableInputs: deprecated("DOM.editableInputs", { get: function editableInputs(elem) DOM.editableInputs }), + escapeHTML: deprecated("DOM.escapeHTML", { get: function escapeHTML(elem) DOM.escapeHTML }), + evaluateXPath: deprecated("DOM.XPath", + function evaluateXPath(path, elem, asIterator) DOM.XPath(path, elem || util.activeWindow.content.document, asIterator)), + isVisible: deprecated("DOM#isVisible", function isVisible(elem) DOM(elem).isVisible), + makeXPath: deprecated("DOM.makeXPath", { get: function makeXPath(elem) DOM.makeXPath }), + namespaces: deprecated("DOM.namespaces", { get: function namespaces(elem) DOM.namespaces }), + namespaceNames: deprecated("DOM.namespaceNames", { get: function namespaceNames(elem) DOM.namespaceNames }), + parseForm: deprecated("DOM#formData", function parseForm(elem) values(DOM(elem).formData).toArray()), + scrollIntoView: deprecated("DOM#scrollIntoView", function scrollIntoView(elem, alignWithTop) DOM(elem).scrollIntoView(alignWithTop)), + validateMatcher: deprecated("DOM.validateMatcher", { get: function validateMatcher() DOM.validateMatcher }), - return update( - function matcher(node) { - if (matcher.xpath) - for (let elem in util.evaluateXPath(matcher.xpath, node)) - yield elem; - - if (matcher.css) - for (let [, elem] in iter(node.querySelectorAll(matcher.css))) - yield elem; - }, { - css: css.join(", "), - xpath: xpath.join(" | ") - }); - }, - - /** - * Validates a list as input for {@link #compileMatcher}. Returns - * true if and only if every element of the list is a valid XPath or - * CSS selector. - * - * @param {[string]} list The list of patterns to test - * @returns {boolean} True when the patterns are all valid. - */ - validateMatcher: function validateMatcher(list) { - let evaluator = services.XPathEvaluator(); - let node = services.XMLDocument(); - return this.testValues(list, function (value) { - if (/^xpath:/.test(value)) - evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver); - else - node.querySelector(value); - return true; - }); - }, - - /** - * Returns an object representing a Node's computed CSS style. - * - * @param {Node} node - * @returns {Object} - */ - computedStyle: function computedStyle(node) { - while (!(node instanceof Ci.nsIDOMElement) && node.parentNode) - node = node.parentNode; - try { - var res = node.ownerDocument.defaultView.getComputedStyle(node, null); - } - catch (e) {} - if (res == null) { - util.dumpStack(_("error.nullComputedStyle", node)); - Cu.reportError(Error(_("error.nullComputedStyle", node))); - return {}; - } - return res; - }, + chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }), + haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.closure.haveGecko }), + OS: deprecated("config.OS", { get: function OS() config.OS }), /** * Converts any arbitrary string into an URI object. Returns null on @@ -614,44 +510,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0), /** - * Converts a given DOM Node, Range, or Selection to a string. If - * *html* is true, the output is HTML, otherwise it is presentation - * text. - * - * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to - * stringify. - * @param {boolean} html Whether the output should be HTML rather - * than presentation text. - */ - domToString: function (node, html) { - if (node instanceof Ci.nsISelection && node.isCollapsed) - return ""; - - if (node instanceof Ci.nsIDOMNode) { - let range = node.ownerDocument.createRange(); - range.selectNode(node); - node = range; - } - let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer; - doc = doc.ownerDocument || doc; - - let encoder = services.HtmlEncoder(); - encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted); - if (node instanceof Ci.nsISelection) - encoder.setSelection(node); - else if (node instanceof Ci.nsIDOMRange) - encoder.setRange(node); - - let str = services.String(encoder.encodeToString()); - if (html) - return str.data; - - let [result, length] = [{}, {}]; - services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length); - return result.value.QueryInterface(Ci.nsISupportsString).data; - }, - - /** * Prints a message to the console. If *msg* is an object it is pretty * printed. * @@ -688,25 +546,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, /** - * The set of input element type attribute values that mark the element as - * an editable field. - */ - editableInputs: Set(["date", "datetime", "datetime-local", "email", "file", - "month", "number", "password", "range", "search", - "tel", "text", "time", "url", "week"]), - - /** - * Converts HTML special characters in *str* to the equivalent HTML - * entities. - * - * @param {string} str - * @returns {string} - */ - escapeHTML: function escapeHTML(str) { - return str.replace(/&/g, "&").replace(/</g, "<"); - }, - - /** * Escapes quotes, newline and tab characters in *str*. The returned string * is delimited by *delimiter* or " if *delimiter* is not specified. * {@see String#quote}. @@ -721,92 +560,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter; }, - /** - * Evaluates an XPath expression in the current or provided - * document. It provides the xhtml, xhtml2 and dactyl XML - * namespaces. The result may be used as an iterator. - * - * @param {string} expression The XPath expression to evaluate. - * @param {Node} elem The context element. - * @default The current document. - * @param {boolean} asIterator Whether to return the results as an - * XPath iterator. - * @returns {Object} Iterable result of the evaluation. - */ - evaluateXPath: update( - function evaluateXPath(expression, elem, asIterator) { - try { - if (!elem) - elem = util.activeWindow.content.document; - let doc = elem.ownerDocument || elem; - if (isArray(expression)) - expression = util.makeXPath(expression); - - let result = doc.evaluate(expression, elem, - evaluateXPath.resolver, - asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, - null - ); - - return Object.create(result, { - __iterator__: { - value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; } - : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); } - } - }); - } - catch (e) { - throw e.stack ? e : Error(e); - } - }, - { - resolver: function lookupNamespaceURI(prefix) (util.namespaces[prefix] || null) - }), - - /** - * Generates an XPath expression for the given element. - * - * @param {Element} elem The element for which to generate an XPath. - * @returns {string} - */ - generateXPath: function generateXPath(elem) { - function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'"; - - let res = []; - let doc = elem.ownerDocument; - for (;; elem = elem.parentNode) { - if (!(elem instanceof Ci.nsIDOMElement)) - res.push(""); - else if (elem.id) - res.push("id(" + quote(elem.id) + ")"); - else { - let name = elem.localName; - if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion)) - if (elem.namespaceURI in this.namespaceNames) - name = this.namespaceNames[elem.namespaceURI] + ":" + name; - else - name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]"; - - res.push(name + "[" + (1 + iter(this.evaluateXPath("./" + name, elem.parentNode)).indexOf(elem)) + "]"); - continue; - } - break; - } - - return res.reverse().join("/"); - }, - - namespaces: { - xul: XUL.uri, - xhtml: XHTML.uri, - html: XHTML.uri, - xhtml2: "http://www.w3.org/2002/06/xhtml2", - dactyl: NS.uri - }, - - namespaceNames: Class.memoize(function () - iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()), - extend: function extend(dest) { Array.slice(arguments, 1).filter(util.identity).forEach(function (src) { for (let [k, v] in Iterator(src)) { @@ -925,15 +678,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, /** - * Returns true if the current Gecko runtime is of the given version - * or greater. - * - * @param {string} ver The required version. - * @returns {boolean} - */ - haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0, - - /** * Sends a synchronous or asynchronous HTTP request to *url* and returns * the XMLHttpRequest object. If *callback* is specified the request is * asynchronous and the *callback* is invoked with the object as its @@ -1017,24 +761,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), */ isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain), - /** Dactyl's notion of the current operating system platform. */ - OS: memoize({ - _arch: services.runtime.OS, - /** - * @property {string} The normalised name of the OS. This is one of - * "Windows", "Mac OS X" or "Unix". - */ - get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix", - /** @property {boolean} True if the OS is Windows. */ - get isWindows() this._arch == "WINNT", - /** @property {boolean} True if the OS is Mac OS X. */ - get isMacOSX() this._arch == "Darwin", - /** @property {boolean} True if the OS is some other *nix variant. */ - get isUnix() !this.isWindows && !this.isMacOSX, - /** @property {RegExp} A RegExp which matches illegal characters in path components. */ - get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g - }), - /** * Returns true if *host* is a subdomain of *domain*. * @@ -1050,17 +776,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, /** - * Returns true if the given DOM node is currently visible. - * - * @param {Node} node - * @returns {boolean} - */ - isVisible: function (node) { - let style = util.computedStyle(node); - return style.visibility == "visible" && style.display != "none"; - }, - - /** * Iterates over all currently open documents, including all * top-level window and sub-frames thereof. */ @@ -1110,20 +825,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, /** - * Returns an XPath union expression constructed from the specified node - * tests. An expression is built with node tests for both the null and - * XHTML namespaces. See {@link Buffer#evaluateXPath}. - * - * @param nodes {Array(string)} - * @returns {string} - */ - makeXPath: function makeXPath(nodes) { - return array(nodes).map(util.debrace).flatten() - .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten() - .map(function (node) "//" + node).join(" | "); - }, - - /** * Creates a DTD fragment from the given object. Each property of * the object is converted to an ENTITY declaration. SGML special * characters other than ' and % are left intact. @@ -1150,7 +851,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), * @param {string} uri * @returns {nsIURI} */ - // FIXME: createURI needed too? newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base), /** @@ -1181,43 +881,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), if (!isObject(object)) return String(object); - function namespaced(node) { - var ns = util.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0]; - if (!ns) - return node.localName; - if (color) - return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</> - return ns + ":" + node.localName; - } - if (object instanceof Ci.nsIDOMElement) { let elem = object; if (elem.nodeType == elem.TEXT_NODE) return elem.data; - try { - let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling) - if (color) - return <span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart"><{ - namespaced(elem)} { - template.map(array.iterValues(elem.attributes), - function (attr) - <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> + - <span highlight="HelpXMLString">{attr.value}</span>, - <> </>) - }{ !hasChildren ? "/>" : ">" - }</span>{ !hasChildren ? "" : <>...</> + - <span highlight="HtmlTagEnd"><{namespaced(elem)}></span> - }</span>; - - let tag = "<" + [namespaced(elem)].concat( - [namespaced(a) + "=" + template.highlight(a.value, true) - for ([i, a] in array.iterItems(elem.attributes))]).join(" "); - return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"); - } - catch (e) { - return {}.toString.call(elem); - } + return DOM(elem).repr(color); } try { // for window.JSON @@ -1487,68 +1156,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, /** - * Parses the fields of a form and returns a URL/POST-data pair - * that is the equivalent of submitting the form. - * - * @param {nsINode} field One of the fields of the given form. - * @returns {array} - */ - // Nuances gleaned from browser.jar/content/browser/browser.js - parseForm: function parseForm(field) { - function encode(name, value, param) { - param = param ? "%s" : ""; - if (post) - return name + "=" + encodeComponent(value + param); - return encodeComponent(name) + "=" + encodeComponent(value) + param; - } - - let form = field.form; - let doc = form.ownerDocument; - - let charset = doc.characterSet; - let converter = services.CharsetConv(charset); - for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) { - let c = services.CharsetConv(cs); - if (c) { - converter = services.CharsetConv(cs); - charset = cs; - } - } - - let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset); - let url = util.newURI(form.action, charset, uri).spec; - - let post = form.method.toUpperCase() == "POST"; - - let encodeComponent = encodeURIComponent; - if (charset !== "UTF-8") - encodeComponent = function encodeComponent(str) - escape(converter.ConvertFromUnicode(str) + converter.Finish()); - - let elems = []; - if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit") - elems.push(encode(field.name, field.value)); - - for (let [, elem] in iter(form.elements)) - if (elem.name && !elem.disabled) { - if (Set.has(util.editableInputs, elem.type) - || /^(?:hidden|textarea)$/.test(elem.type) - || elem.type == "submit" && elem == field - || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) - elems.push(encode(elem.name, elem.value, elem === field)); - else if (elem instanceof Ci.nsIDOMHTMLSelectElement) { - for (let [, opt] in Iterator(elem.options)) - if (opt.selected) - elems.push(encode(elem.name, opt.value)); - } - } - - if (post) - return [url, elems.join('&'), charset, elems]; - return [url + "?" + elems.join('&'), null, charset, elems]; - }, - - /** * A generator that returns the values between *start* and *end*, in *step* * increments. * @@ -1790,19 +1397,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), }, /** - * Scrolls an element into view if and only if it's not already - * fully visible. - * - * @param {Node} elem The element to make visible. - */ - scrollIntoView: function scrollIntoView(elem, alignWithTop) { - let win = elem.ownerDocument.defaultView; - let rect = elem.getBoundingClientRect(); - if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0)) - elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom)); - }, - - /** * Returns the selection controller for the given window. * * @param {Window} window @@ -1814,6 +1408,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), .QueryInterface(Ci.nsISelectionController), /** + * Escapes a string against shell meta-characters and argument + * separators. + */ + shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$]/g, "\\$&") + '"', + + /** * Suspend execution for at least *delay* milliseconds. Functions by * yielding execution to the next item in the main event queue, and * so may lead to unexpected call graphs, and long delays if another @@ -2139,6 +1739,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), Array: array }); + +/** + * @class + * + * A jQuery-inspired DOM utility framework. + * + * Please note that while this currently implements an Array-like + * interface, this is *not a defined interface* and is very likely to + * change in the near future. + */ var DOM = Class("DOM", { init: function init(val, context) { let self; @@ -2153,12 +1763,15 @@ var DOM = Class("DOM", { if (val == null) ; else if (typeof val == "xml") - this[length++] = util.xmlToDom(val, context); - else if (val instanceof Ci.nsIDOMNode) + this[length++] = util.xmlToDom(val, context, this.nodes); + else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow) this[length++] = val; else if ("length" in val) for (let i = 0; i < val.length; i++) this[length++] = val[i]; + else if ("__iterator__" in val) + for (let elem in val) + this[length++] = elem; this.length = length; return self || this; @@ -2171,6 +1784,8 @@ var DOM = Class("DOM", { Empty: function Empty() this.constructor(null, this.document), + nodes: Class.memoize(function () ({})), + get items() { for (let i = 0; i < this.length; i++) yield this.eq(i); @@ -2234,10 +1849,10 @@ var DOM = Class("DOM", { if (callable(val)) return this.each(function (elem, i) { - fn.call(this, munge(val.call(this, elem, i)), elem, i); + util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i); }, self || this); - fn.call(self || this, munge(val), this[0], 0); + util.withProperErrors(fn, self || this, munge(val), this[0], 0); return this; }, @@ -2276,8 +1891,16 @@ var DOM = Class("DOM", { let res = this.Empty(); this.each(function (elem) { - while((elem = fn.call(this, elem)) instanceof Ci.nsIDOMElement) - res[res.length++] = elem; + while(true) { + elem = fn.call(this, elem) + if (elem instanceof Ci.nsIDOMElement) + res[res.length++] = elem; + else if (elem && "length" in elem) + for (let i = 0; i < tmp.length; i++) + res[res.length++] = tmp[j]; + else + break; + } }, self || this); return res; }, @@ -2288,10 +1911,10 @@ var DOM = Class("DOM", { for (let i = 0; i < this.length; i++) { let tmp = fn.call(self || update(obj, [this[i]]), this[i], i); - if ("length" in tmp) + if (tmp && "length" in tmp) for (let j = 0; j < tmp.length; j++) res[res.length++] = tmp[j]; - else + else if (tmp !== undefined) res[res.length++] = tmp; } @@ -2311,6 +1934,15 @@ var DOM = Class("DOM", { get parent() this.map(function (elem) elem.parentNode, this), + get offsetParent() this.map(function (elem) { + do { + var parent = elem.offsetParent; + if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static") + return parent; + } + while (parent); + }, this), + get ancestors() this.all(function (elem) elem.parentNode), get children() this.map(function (elem) Array.filter(elem.childNodes, @@ -2354,6 +1986,7 @@ var DOM = Class("DOM", { has: function has(hl) ~this.list.indexOf(hl), add: function add(hl) self.each(function () { + highlight.loaded[hl] = true; this.attrNS(NS, "highlight", array.uniq(this.highlight.list.concat(hl)).join(" ")); }), @@ -2371,7 +2004,213 @@ var DOM = Class("DOM", { get rect() this[0].getBoundingClientRect(), - get style() util.computedStyle(this[0]), + get viewport() { + let r = this.rect; + return { + width: this[0].clientWidth, + height: this[0].clientHeight, + top: r.top + this[0].clientTop, + get bottom() this.top + this.height, + left: r.left + this[0].clientLeft, + get right() this.left + this.width + } + }, + + /** + * Returns true if the given DOM node is currently visible. + * @returns {boolean} + */ + get isVisible() { + let style = this.style; + return style.visibility == "visible" && style.display != "none"; + }, + + get editor() { + this[0] instanceof Ci.nsIDOMNSEditableElement; + if (this[0].editor instanceof Ci.nsIEditor) + return this[0].editor; + + try { + return this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession) + .getEditorForWindow(this[0]); + } + catch (e) {} + + return null; + }, + + get isEditable() !!this.editor, + + get isInput() this[0] instanceof Ci.nsIDOMHTMLInputElement && this.isEditable, + + /** + * Returns an object representing a Node's computed CSS style. + * @returns {Object} + */ + get style() { + let node = this[0]; + while (!(node instanceof Ci.nsIDOMElement) && node.parentNode) + node = node.parentNode; + + try { + var res = node.ownerDocument.defaultView.getComputedStyle(node, null); + } + catch (e) {} + + if (res == null) { + util.dumpStack(_("error.nullComputedStyle", node)); + Cu.reportError(Error(_("error.nullComputedStyle", node))); + return {}; + } + return res; + }, + + /** + * Parses the fields of a form and returns a URL/POST-data pair + * that is the equivalent of submitting the form. + * + * @returns {object} An object with the following elements: + * url: The URL the form points to. + * postData: A string containing URL-encoded post data, if this + * form is to be POSTed + * charset: The character set of the GET or POST data. + * elements: The key=value pairs used to generate query information. + */ + // Nuances gleaned from browser.jar/content/browser/browser.js + get formData() { + function encode(name, value, param) { + param = param ? "%s" : ""; + if (post) + return name + "=" + encodeComponent(value + param); + return encodeComponent(name) + "=" + encodeComponent(value) + param; + } + + let field = this[0]; + let form = field.form; + let doc = form.ownerDocument; + + let charset = doc.characterSet; + let converter = services.CharsetConv(charset); + for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) { + let c = services.CharsetConv(cs); + if (c) { + converter = services.CharsetConv(cs); + charset = cs; + } + } + + let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset); + let url = util.newURI(form.action, charset, uri).spec; + + let post = form.method.toUpperCase() == "POST"; + + let encodeComponent = encodeURIComponent; + if (charset !== "UTF-8") + encodeComponent = function encodeComponent(str) + escape(converter.ConvertFromUnicode(str) + converter.Finish()); + + let elems = []; + if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit") + elems.push(encode(field.name, field.value)); + + for (let [, elem] in iter(form.elements)) + if (elem.name && !elem.disabled) { + if (DOM(elem).isInput + || /^(?:hidden|textarea)$/.test(elem.type) + || elem.type == "submit" && elem == field + || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) + elems.push(encode(elem.name, elem.value, elem === field)); + else if (elem instanceof Ci.nsIDOMHTMLSelectElement) { + for (let [, opt] in Iterator(elem.options)) + if (opt.selected) + elems.push(encode(elem.name, opt.value)); + } + } + + if (post) + return { url: url, postData: elems.join('&'), charset: charset, elements: elems }; + return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems }; + }, + + /** + * Generates an XPath expression for the given element. + * + * @returns {string} + */ + get xpath() { + function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'"; + + let res = []; + let doc = this.document; + for (let elem = this[0];; elem = elem.parentNode) { + if (!(elem instanceof Ci.nsIDOMElement)) + res.push(""); + else if (elem.id) + res.push("id(" + quote(elem.id) + ")"); + else { + let name = elem.localName; + if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion)) + if (elem.namespaceURI in DOM.namespaceNames) + name = DOM.namespaceNames[elem.namespaceURI] + ":" + name; + else + name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]"; + + res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]"); + continue; + } + break; + } + + return res.reverse().join("/"); + }, + + /** + * Returns a string or XML representation of this node. + * + * @param {boolean} color If true, return a colored, XML + * representation of this node. + */ + repr: function repr(color) { + function namespaced(node) { + var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0]; + if (!ns) + return node.localName; + if (color) + return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</> + return ns + ":" + node.localName; + } + + let res = []; + this.each(function (elem) { + try { + let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling) + if (color) + res.push(<span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart"><{ + namespaced(elem)} { + template.map(array.iterValues(elem.attributes), + function (attr) + <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> + + <span highlight="HelpXMLString">{attr.value}</span>, + <> </>) + }{ !hasChildren ? "/>" : ">" + }</span>{ !hasChildren ? "" : <>...</> + + <span highlight="HtmlTagEnd"><{namespaced(elem)}></span> + }</span>); + else { + let tag = "<" + [namespaced(elem)].concat( + [namespaced(a) + "=" + template.highlight(a.value, true) + for ([i, a] in array.iterItems(elem.attributes))]).join(" "); + + res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">")); + } + } + catch (e) { + res.push({}.toString.call(elem)); + } + }, this); + return template.map(res, util.identity, <>,</>); + }, attr: function attr(key, val) { return this.attrNS("", key, val); @@ -2574,7 +2413,32 @@ var DOM = Class("DOM", { if (callable(arg)) return this.listen("blur", arg, extra); return this.each(function (elem) { elem.blur(); }, this); - } + }, + + /** + * Scrolls an element into view if and only if it's not already + * fully visible. + */ + scrollIntoView: function scrollIntoView(alignWithTop) { + return this.each(function (elem) { + let rect = this.rect; + + let force = false; + if (rect) + for (let parent in this.ancestors.items) { + let isect = util.intersection(rect, parent.viewport); + force = isect.width != rect.width || isect.height != rect.height; + if (force) + break; + } + + let win = this.document.defaultView; + + if (force || !(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0)) + elem.scrollIntoView(arguments.length ? alignWithTop + : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom)); + }); + }, }, { /** * Creates an actual event from a pseudo-event object. @@ -2643,7 +2507,7 @@ var DOM = Class("DOM", { * @param {Event} event The event to dispatch. */ dispatch: Class.memoize(function () - util.haveGecko("2b") + config.haveGecko("2b") ? function dispatch(target, event, extra) { try { this.feedingEvent = extra; @@ -2674,7 +2538,184 @@ var DOM = Class("DOM", { this.feedingEvent = null; } }) - }) + }), + + /** + * The set of input element type attribute values that mark the element as + * an editable field. + */ + editableInputs: Set(["date", "datetime", "datetime-local", "email", "file", + "month", "number", "password", "range", "search", + "tel", "text", "time", "url", "week"]), + + /** + * Converts a given DOM Node, Range, or Selection to a string. If + * *html* is true, the output is HTML, otherwise it is presentation + * text. + * + * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to + * stringify. + * @param {boolean} html Whether the output should be HTML rather + * than presentation text. + */ + stringify: function stringify(node, html) { + if (node instanceof Ci.nsISelection && node.isCollapsed) + return ""; + + if (node instanceof Ci.nsIDOMNode) { + let range = node.ownerDocument.createRange(); + range.selectNode(node); + node = range; + } + let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer; + doc = doc.ownerDocument || doc; + + let encoder = services.HtmlEncoder(); + encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted); + if (node instanceof Ci.nsISelection) + encoder.setSelection(node); + else if (node instanceof Ci.nsIDOMRange) + encoder.setRange(node); + + let str = services.String(encoder.encodeToString()); + if (html) + return str.data; + + let [result, length] = [{}, {}]; + services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length); + return result.value.QueryInterface(Ci.nsISupportsString).data; + }, + + /** + * Compiles a CSS spec and XPath pattern matcher based on the given + * list. List elements prefixed with "xpath:" are parsed as XPath + * patterns, while other elements are parsed as CSS specs. The + * returned function will, given a node, return an iterator of all + * descendants of that node which match the given specs. + * + * @param {[string]} list The list of patterns to match. + * @returns {function(Node)} + */ + compileMatcher: function compileMatcher(list) { + let xpath = [], css = []; + for (let elem in values(list)) + if (/^xpath:/.test(elem)) + xpath.push(elem.substr(6)); + else + css.push(elem); + + return update( + function matcher(node) { + if (matcher.xpath) + for (let elem in DOM.XPath(matcher.xpath, node)) + yield elem; + + if (matcher.css) + for (let [, elem] in iter(node.querySelectorAll(matcher.css))) + yield elem; + }, { + css: css.join(", "), + xpath: xpath.join(" | ") + }); + }, + + /** + * Validates a list as input for {@link #compileMatcher}. Returns + * true if and only if every element of the list is a valid XPath or + * CSS selector. + * + * @param {[string]} list The list of patterns to test + * @returns {boolean} True when the patterns are all valid. + */ + validateMatcher: function validateMatcher(list) { + let evaluator = services.XPathEvaluator(); + let node = services.XMLDocument(); + return this.testValues(list, function (value) { + if (/^xpath:/.test(value)) + evaluator.createExpression(value.substr(6), DOM.XPath.resolver); + else + node.querySelector(value); + return true; + }); + }, + + /** + * Converts HTML special characters in *str* to the equivalent HTML + * entities. + * + * @param {string} str + * @returns {string} + */ + escapeHTML: function escapeHTML(str) { + let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" }; + return str.replace(/['"&<>]/g, function (m) map[m]); + }, + + + /** + * Evaluates an XPath expression in the current or provided + * document. It provides the xhtml, xhtml2 and dactyl XML + * namespaces. The result may be used as an iterator. + * + * @param {string} expression The XPath expression to evaluate. + * @param {Node} elem The context element. + * @param {boolean} asIterator Whether to return the results as an + * XPath iterator. + * @returns {Object} Iterable result of the evaluation. + */ + XPath: update( + function XPath(expression, elem, asIterator) { + try { + let doc = elem.ownerDocument || elem; + + if (isArray(expression)) + expression = DOM.makeXPath(expression); + + let result = doc.evaluate(expression, elem, + XPath.resolver, + asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + + return Object.create(result, { + __iterator__: { + value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; } + : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); } + } + }); + } + catch (e) { + throw e.stack ? e : Error(e); + } + }, + { + resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null) + }), + + /** + * Returns an XPath union expression constructed from the specified node + * tests. An expression is built with node tests for both the null and + * XHTML namespaces. See {@link DOM.XPath}. + * + * @param nodes {Array(string)} + * @returns {string} + */ + makeXPath: function makeXPath(nodes) { + return array(nodes).map(util.debrace).flatten() + .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten() + .map(function (node) "//" + node).join(" | "); + }, + + namespaces: { + xul: XUL.uri, + xhtml: XHTML.uri, + html: XHTML.uri, + xhtml2: "http://www.w3.org/2002/06/xhtml2", + dactyl: NS.uri + }, + + namespaceNames: Class.memoize(function () + iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()), }); Object.keys(DOM.Event.types).forEach(function (event) { |