1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2013 Kris Maglione <maglione.k@gmail.com>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
9 exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
12 lazyRequire("highlight", ["highlight"]);
13 lazyRequire("messages", ["_"]);
14 lazyRequire("overlay", ["overlay"]);
15 lazyRequire("prefs", ["prefs"]);
16 lazyRequire("template", ["template"]);
18 var XBL = "http://www.mozilla.org/xbl";
19 var XHTML = "http://www.w3.org/1999/xhtml";
20 var XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
21 var NS = "http://vimperator.org/namespaces/liberator";
23 function BooleanAttribute(attr) ({
24 get: function (elem) elem.getAttribute(attr) == "true",
25 set: function (elem, val) {
26 if (val === "false" || !val)
27 elem.removeAttribute(attr);
29 elem.setAttribute(attr, true);
36 * A jQuery-inspired DOM utility framework.
38 * Please note that while this currently implements an Array-like
39 * interface, this is *not a defined interface* and is very likely to
40 * change in the near future.
42 var DOM = Class("DOM", {
43 init: function init(val, context, nodes) {
50 if (context instanceof Ci.nsIDOMDocument)
51 this.document = context;
53 if (typeof val == "string")
54 val = context.querySelectorAll(val);
58 else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
59 this[length++] = DOM.fromXML(val, context, this.nodes);
60 else if (DOM.isJSONXML(val)) {
61 if (context instanceof Ci.nsIDOMDocument)
62 this[length++] = DOM.fromJSON(val, context, this.nodes);
66 else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
68 else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
70 this[length++] = elem;
71 else if ("length" in val)
72 for (let i = 0; i < val.length; i++)
73 this[length++] = val[i];
81 __iterator__: function __iterator__() {
82 for (let i = 0; i < this.length; i++)
86 Empty: function Empty() this.constructor(null, this.document),
88 nodes: Class.Memoize(function () ({})),
91 for (let i = 0; i < this.length; i++)
95 get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
96 set document(val) this._document = val,
98 attrHooks: array.toObject([
100 href: { get: function (elem) elem.href || elem.getAttribute("href") },
101 src: { get: function (elem) elem.src || elem.getAttribute("src") },
102 checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
103 set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val; } },
104 collapsed: BooleanAttribute("collapsed"),
105 disabled: BooleanAttribute("disabled"),
106 hidden: BooleanAttribute("hidden"),
107 readonly: BooleanAttribute("readonly")
111 matcher: function matcher(sel) elem => (elem.mozMatchesSelector && elem.mozMatchesSelector(sel)),
113 each: function each(fn, self) {
114 let obj = self || this.Empty();
115 for (let i = 0; i < this.length; i++)
116 fn.call(self || update(obj, [this[i]]), this[i], i);
120 eachDOM: function eachDOM(val, fn, self) {
122 function munge(val, container, idx) {
123 if (val instanceof Ci.nsIDOMRange)
124 return val.extractContents();
125 if (val instanceof Ci.nsIDOMNode)
128 if (DOM.isJSONXML(val)) {
129 val = dom.constructor(val, dom.document);
131 container[idx] = val[0];
134 if (isObject(val) && "length" in val) {
135 let frag = dom.document.createDocumentFragment();
136 for (let i = 0; i < val.length; i++)
137 frag.appendChild(munge(val[i], val, i));
143 if (DOM.isJSONXML(val))
144 val = (function () this).bind(val);
147 return this.each(function (elem, i) {
148 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
152 util.withProperErrors(fn, self || this, munge(val), this[0], 0);
156 eq: function eq(idx) {
157 return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
160 find: function find(val) {
161 return this.map(elem => elem.querySelectorAll(val));
164 findAnon: function findAnon(attr, val) {
165 return this.map(elem => elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
168 filter: function filter(val, self) {
169 let res = this.Empty();
172 val = this.matcher(val);
174 this.constructor(Array.filter(this, val, self || this));
175 let obj = self || this.Empty();
176 for (let i = 0; i < this.length; i++)
177 if (val.call(self || update(obj, [this[i]]), this[i], i))
178 res[res.length++] = this[i];
183 is: function is(val) {
184 return this.some(this.matcher(val));
187 reverse: function reverse() {
192 all: function all(fn, self) {
193 let res = this.Empty();
195 this.each(function (elem) {
197 elem = fn.call(this, elem);
198 if (elem instanceof Ci.nsIDOMNode)
199 res[res.length++] = elem;
200 else if (elem && "length" in elem)
201 for (let i = 0; i < elem.length; i++)
202 res[res.length++] = elem[j];
210 map: function map(fn, self) {
211 let res = this.Empty();
212 let obj = self || this.Empty();
214 for (let i = 0; i < this.length; i++) {
215 let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
216 if (isObject(tmp) && !(tmp instanceof Ci.nsIDOMNode) && "length" in tmp)
217 for (let j = 0; j < tmp.length; j++)
218 res[res.length++] = tmp[j];
219 else if (tmp != null)
220 res[res.length++] = tmp;
226 slice: function eq(start, end) {
227 return this.constructor(Array.slice(this, start, end));
230 some: function some(fn, self) {
231 for (let i = 0; i < this.length; i++)
232 if (fn.call(self || this, this[i], i))
237 get parent() this.map(elem => elem.parentNode, this),
239 get offsetParent() this.map(function (elem) {
241 var parent = elem.offsetParent;
242 if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
248 get ancestors() this.all(elem => elem.parentNode),
250 get children() this.map(elem => Array.filter(elem.childNodes,
251 e => e instanceof Ci.nsIDOMElement),
254 get contents() this.map(elem => elem.childNodes, this),
256 get siblings() this.map(elem => Array.filter(elem.parentNode.childNodes,
257 e => e != elem && e instanceof Ci.nsIDOMElement),
260 get siblingsBefore() this.all(elem => elem.previousElementSibling),
261 get siblingsAfter() this.all(elem => elem.nextElementSibling),
263 get allSiblingsBefore() this.all(elem => elem.previousSibling),
264 get allSiblingsAfter() this.all(elem => elem.nextSibling),
266 get class() let (self = this) ({
267 toString: function () self[0].className,
269 get list() Array.slice(self[0].classList),
270 set list(val) self.attr("class", val.join(" ")),
272 each: function each(meth, arg) {
273 return self.each(function (elem) {
274 elem.classList[meth](arg);
278 add: function add(cls) this.each("add", cls),
279 remove: function remove(cls) this.each("remove", cls),
280 toggle: function toggle(cls, val, thisObj) {
282 return self.each(function (elem, i) {
283 this.class.toggle(cls, val.call(thisObj || this, elem, i));
285 return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
288 has: function has(cls) this[0].classList.has(cls)
291 get highlight() let (self = this) ({
292 toString: function () self.attrNS(NS, "highlight") || "",
294 get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
296 let str = array.uniq(val).join(" ").trim();
297 self.attrNS(NS, "highlight", str || null);
300 has: function has(hl) ~this.list.indexOf(hl),
302 add: function add(hl) self.each(function () {
303 highlight.loaded[hl] = true;
304 this.highlight.list = this.highlight.list.concat(hl);
307 remove: function remove(hl) self.each(function () {
308 this.highlight.list = this.highlight.list.filter(h => h != hl);
311 toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
312 let { highlight } = this;
313 let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
315 highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl);
319 get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
320 height: this[0].scrollMaxY + this[0].innerHeight,
321 get right() this.width + this.left,
322 get bottom() this.height + this.top,
323 top: -this[0].scrollY,
324 left: -this[0].scrollX } :
325 this[0] ? this[0].getBoundingClientRect() : {},
329 if (node instanceof Ci.nsIDOMDocument)
330 node = node.defaultView;
332 if (node instanceof Ci.nsIDOMWindow)
334 get width() this.right - this.left,
335 get height() this.bottom - this.top,
336 bottom: node.innerHeight,
337 right: node.innerWidth,
343 width: node.clientWidth,
344 height: node.clientHeight,
345 top: r.top + node.clientTop,
346 get bottom() this.top + this.height,
347 left: r.left + node.clientLeft,
348 get right() this.left + this.width
352 scrollPos: function scrollPos(left, top) {
353 if (arguments.length == 0) {
354 if (this[0] instanceof Ci.nsIDOMElement)
355 return { top: this[0].scrollTop, left: this[0].scrollLeft,
356 height: this[0].scrollHeight, width: this[0].scrollWidth,
357 innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
359 if (this[0] instanceof Ci.nsIDOMWindow)
360 return { top: this[0].scrollY, left: this[0].scrollX,
361 height: this[0].scrollMaxY + this[0].innerHeight,
362 width: this[0].scrollMaxX + this[0].innerWidth,
363 innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
367 let func = callable(left) && left;
369 return this.each(function (elem, i) {
371 ({ left, top }) = func.call(this, elem, i);
373 if (elem instanceof Ci.nsIDOMWindow)
374 elem.scrollTo(left == null ? elem.scrollX : left,
375 top == null ? elem.scrollY : top);
378 elem.scrollLeft = left;
380 elem.scrollTop = top;
386 * Returns true if the given DOM node is currently visible.
390 let style = this[0] && this.style;
391 return style && style.visibility == "visible" && style.display != "none";
398 this[0] instanceof Ci.nsIDOMNSEditableElement;
400 if (this[0].editor instanceof Ci.nsIEditor)
401 var editor = this[0].editor;
409 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
410 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
411 .getEditorForWindow(this[0]);
415 editor instanceof Ci.nsIPlaintextEditor;
416 editor instanceof Ci.nsIHTMLEditor;
420 get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
422 get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
423 Ci.nsIDOMHTMLTextAreaElement,
424 Ci.nsIDOMXULTextBoxElement])
428 * Returns an object representing a Node's computed CSS style.
433 if (node instanceof Ci.nsIDOMWindow)
434 node = node.document;
435 if (node instanceof Ci.nsIDOMDocument)
436 node = node.documentElement;
437 while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
438 node = node.parentNode;
441 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
446 util.dumpStack(_("error.nullComputedStyle", node));
447 Cu.reportError(Error(_("error.nullComputedStyle", node)));
454 * Parses the fields of a form and returns a URL/POST-data pair
455 * that is the equivalent of submitting the form.
457 * @returns {object} An object with the following elements:
458 * url: The URL the form points to.
459 * postData: A string containing URL-encoded post data, if this
460 * form is to be POSTed
461 * charset: The character set of the GET or POST data.
462 * elements: The key=value pairs used to generate query information.
464 // Nuances gleaned from browser.jar/content/browser/browser.js
466 function encode(name, value, param) {
467 param = param ? "%s" : "";
469 return name + "=" + encodeComponent(value + param);
470 return encodeComponent(name) + "=" + encodeComponent(value) + param;
474 let form = field.form;
475 let doc = form.ownerDocument;
477 let charset = doc.characterSet;
478 let converter = services.CharsetConv(charset);
479 for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
480 let c = services.CharsetConv(cs);
482 converter = services.CharsetConv(cs);
487 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
488 let url = util.newURI(form.action, charset, uri).spec;
490 let post = form.method.toUpperCase() == "POST";
492 let encodeComponent = encodeURIComponent;
493 if (charset !== "UTF-8")
494 encodeComponent = function encodeComponent(str)
495 escape(converter.ConvertFromUnicode(str) + converter.Finish());
498 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
499 elems.push(encode(field.name, field.value));
501 for (let [, elem] in iter(form.elements))
502 if (elem.name && !elem.disabled) {
503 if (DOM(elem).isInput
504 || /^(?:hidden|textarea)$/.test(elem.type)
505 || elem.type == "submit" && elem == field
506 || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) {
509 elems.push(encode(elem.name, elem.value));
510 else if (overlay.getData(elem, "had-focus"))
511 elems.push(encode(elem.name, elem.value, true));
513 elems.push(encode(elem.name, "", true));
515 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
516 for (let [, opt] in Iterator(elem.options))
518 elems.push(encode(elem.name, opt.value));
523 return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
524 return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
528 * Generates an XPath expression for the given element.
533 function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
534 if (!(this[0] instanceof Ci.nsIDOMElement))
538 let doc = this.document;
539 for (let elem = this[0];; elem = elem.parentNode) {
540 if (!(elem instanceof Ci.nsIDOMElement))
543 res.push("id(" + quote(elem.id) + ")");
545 let name = elem.localName;
546 if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
547 if (elem.namespaceURI in DOM.namespaceNames)
548 name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
550 name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
552 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
558 return res.reverse().join("/");
562 * Returns a string or XML representation of this node.
564 * @param {boolean} color If true, return a colored, XML
565 * representation of this node.
567 repr: function repr(color) {
568 function namespaced(node) {
569 var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
571 return node.localName;
573 return [["span", { highlight: "HelpXMLNamespace" }, ns],
575 return ns + ":" + node.localName;
579 this.each(function (elem) {
581 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling);
583 res.push(["span", { highlight: "HelpXML" },
584 ["span", { highlight: "HelpXMLTagStart" },
585 "<", namespaced(elem), " ",
586 template.map(array.iterValues(elem.attributes),
588 ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
589 ["span", { highlight: "HelpXMLString" }, attr.value]
592 !hasChildren ? "/>" : ">",
596 ["span", { highlight: "HtmlTagEnd" }, "<", namespaced(elem), ">"]]
599 let tag = "<" + [namespaced(elem)].concat(
600 [namespaced(a) + '="' + String.replace(a.value, /["<]/, DOM.escapeHTML) + '"'
601 for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
603 res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
607 res.push({}.toString.call(elem));
610 res = template.map(res, util.identity, ",");
611 return color ? res : res.join("");
614 attr: function attr(key, val) {
615 return this.attrNS("", key, val);
618 attrNS: function attrNS(ns, key, val) {
619 if (val !== undefined)
620 key = array.toObject([[key, val]]);
622 let hooks = this.attrHooks[ns] || {};
625 return this.each(function (elem, i) {
626 for (let [k, v] in Iterator(key)) {
628 v = v.call(this, elem, i);
630 if (Set.has(hooks, k) && hooks[k].set)
631 hooks[k].set.call(this, elem, v, k);
633 elem.removeAttributeNS(ns, k);
635 elem.setAttributeNS(ns, k, v);
642 if (Set.has(hooks, key) && hooks[key].get)
643 return hooks[key].get.call(this, this[0], key);
645 if (!this[0].hasAttributeNS(ns, key))
648 return this[0].getAttributeNS(ns, key);
651 css: update(function css(key, val) {
652 if (val !== undefined)
653 key = array.toObject([[key, val]]);
656 return this.each(function (elem) {
657 for (let [k, v] in Iterator(key))
658 elem.style[css.property(k)] = v;
661 return this[0].style[css.property(key)];
663 name: function (property) property.replace(/[A-Z]/g, m0 => "-" + m0.toLowerCase()),
665 property: function (name) name.replace(/-(.)/g, (m0, m1) => m1.toUpperCase())
668 append: function append(val) {
669 return this.eachDOM(val, function (elem, target) {
670 target.appendChild(elem);
674 prepend: function prepend(val) {
675 return this.eachDOM(val, function (elem, target) {
676 target.insertBefore(elem, target.firstChild);
680 before: function before(val) {
681 return this.eachDOM(val, function (elem, target) {
682 target.parentNode.insertBefore(elem, target);
686 after: function after(val) {
687 return this.eachDOM(val, function (elem, target) {
688 target.parentNode.insertBefore(elem, target.nextSibling);
692 appendTo: function appendTo(elem) {
693 if (!(elem instanceof this.constructor))
694 elem = this.constructor(elem, this.document);
699 prependTo: function prependTo(elem) {
700 if (!(elem instanceof this.constructor))
701 elem = this.constructor(elem, this.document);
706 insertBefore: function insertBefore(elem) {
707 if (!(elem instanceof this.constructor))
708 elem = this.constructor(elem, this.document);
713 insertAfter: function insertAfter(elem) {
714 if (!(elem instanceof this.constructor))
715 elem = this.constructor(elem, this.document);
720 remove: function remove() {
721 return this.each(function (elem) {
723 elem.parentNode.removeChild(elem);
727 empty: function empty() {
728 return this.each(function (elem) {
729 while (elem.firstChild)
730 elem.removeChild(elem.firstChild);
734 fragment: function fragment() {
735 let frag = this.document.createDocumentFragment();
740 clone: function clone(deep)
741 this.map(elem => elem.cloneNode(deep)),
743 toggle: function toggle(val, self) {
745 return this.each(function (elem, i) {
746 this[val.call(self || this, elem, i) ? "show" : "hide"]();
749 if (arguments.length)
750 return this[val ? "show" : "hide"]();
752 let hidden = this.map(elem => elem.style.display == "none");
753 return this.each(function (elem, i) {
754 this[hidden[i] ? "show" : "hide"]();
757 hide: function hide() {
758 return this.each(function (elem) { elem.style.display = "none"; }, this);
760 show: function show() {
761 for (let i = 0; i < this.length; i++)
762 if (!this[i].dactylDefaultDisplay && this[i].style.display)
763 this[i].style.display = "";
765 this.each(function (elem) {
766 if (!elem.dactylDefaultDisplay)
767 elem.dactylDefaultDisplay = this.style.display;
770 return this.each(function (elem) {
771 elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
775 createContents: function createContents()
776 this.each(DOM.createContents, this),
778 isScrollable: function isScrollable(direction)
779 this.length && DOM.isScrollable(this[0], direction),
781 getSet: function getSet(args, get, set) {
783 return this[0] && get.call(this, this[0]);
785 let [fn, self] = args;
789 return this.each(function (elem, i) {
790 set.call(this, elem, fn.call(self || this, elem, i));
794 html: function html(txt, self) {
795 return this.getSet(arguments,
796 elem => elem.innerHTML,
797 util.wrapCallback((elem, val) => { elem.innerHTML = val; }));
800 text: function text(txt, self) {
801 return this.getSet(arguments,
802 elem => elem.textContent,
803 (elem, val) => { elem.textContent = val; });
806 val: function val(txt) {
807 return this.getSet(arguments,
809 (elem, val) => { elem.value = val == null ? "" : val; });
812 listen: function listen(event, listener, capture) {
816 event = array.toObject([[event, listener]]);
818 for (let [evt, callback] in Iterator(event))
819 event[evt] = util.wrapCallback(callback, true);
821 return this.each(function (elem) {
822 for (let [evt, callback] in Iterator(event))
823 elem.addEventListener(evt, callback, capture);
826 unlisten: function unlisten(event, listener, capture) {
830 event = array.toObject([[event, listener]]);
832 return this.each(function (elem) {
833 for (let [k, v] in Iterator(event))
834 elem.removeEventListener(k, v.wrapper || v, capture);
837 once: function once(event, listener, capture) {
841 event = array.toObject([[event, listener]]);
843 for (let pair in Iterator(event)) {
844 let [evt, callback] = pair;
845 event[evt] = util.wrapCallback(function wrapper(event) {
846 this.removeEventListener(evt, wrapper.wrapper, capture);
847 return callback.apply(this, arguments);
851 return this.each(function (elem) {
852 for (let [k, v] in Iterator(event))
853 elem.addEventListener(k, v, capture);
857 dispatch: function dispatch(event, params, extraProps) {
858 this.canceled = false;
859 return this.each(function (elem) {
860 let evt = DOM.Event(this.document, event, params, elem);
861 if (!DOM.Event.dispatch(elem, evt, extraProps))
862 this.canceled = true;
866 focus: function focus(arg, extra) {
868 return this.listen("focus", arg, extra);
871 let flags = arg || services.focus.FLAG_BYMOUSE;
873 if (elem instanceof Ci.nsIDOMDocument)
874 elem = elem.defaultView;
875 if (elem instanceof Ci.nsIDOMElement)
876 services.focus.setFocus(elem, flags);
877 else if (elem instanceof Ci.nsIDOMWindow) {
878 services.focus.focusedWindow = elem;
879 if (services.focus.focusedWindow != elem)
880 services.focus.clearFocus(elem);
889 blur: function blur(arg, extra) {
891 return this.listen("blur", arg, extra);
892 return this.each(function (elem) { elem.blur(); }, this);
896 * Scrolls an element into view if and only if it's not already
899 scrollIntoView: function scrollIntoView(alignWithTop) {
900 return this.each(function (elem) {
901 function getAlignment(viewport) {
902 if (alignWithTop !== undefined)
904 if (rect.bottom < viewport.top)
906 if (rect.top > viewport.bottom)
908 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom);
912 function fix(parent) {
913 if (!(parent[0] instanceof Ci.nsIDOMWindow)
914 && parent.style.overflow == "visible")
917 ({ rect }) = DOM(elem);
918 let { viewport } = parent;
919 let isect = util.intersection(rect, viewport);
921 if (isect.height < Math.min(viewport.height, rect.height)) {
922 let { top } = parent.scrollPos();
923 if (getAlignment(viewport))
924 parent.scrollPos(null, top - (viewport.top - rect.top));
926 parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
931 for (let parent in this.ancestors.items)
934 fix(DOM(this.document.defaultView));
939 * Creates an actual event from a pseudo-event object.
941 * The pseudo-event object (such as may be retrieved from
942 * DOM.Event.parse) should have any properties you want the event to
945 * @param {Document} doc The DOM document to associate this event with
946 * @param {Type} type The type of event (keypress, click, etc.)
947 * @param {Object} opts The pseudo-event. @optional
949 Event: Class("Event", {
950 init: function Event(doc, type, opts, target) {
953 type: type, bubbles: true, cancelable: false
957 bubbles: true, cancelable: true,
958 view: doc.defaultView,
959 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
960 keyCode: 0, charCode: 0
964 bubbles: true, cancelable: true,
965 view: doc.defaultView,
967 get screenX() this.view.mozInnerScreenX
968 + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
969 get screenY() this.view.mozInnerScreenY
970 + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
973 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
980 var t = this.constructor.types[type] || "";
981 var evt = doc.createEvent(t + "Events");
983 let params = DEFAULTS[t || "HTML"];
984 let args = Object.keys(params);
985 update(params, this.constructor.defaults[type],
986 iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
988 evt["init" + t + "Event"].apply(evt, args.map(k => params[k]));
992 init: function init() {
993 // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
994 // matters, so use that string as the first item, that you
995 // want to refer to within dactyl's source code for
996 // comparisons like if (key == "<Esc>") { ... }
998 add: ["+", "Plus", "Add"],
1004 close_bracket: ["]"],
1007 escape: ["Esc", "Escape"],
1008 insert: ["Insert", "Ins"],
1010 left_shift: ["LT", "<"],
1012 open_bracket: ["["],
1016 return: ["Return", "CR", "Enter"],
1020 space: ["Space", " "],
1021 subtract: ["-", "Minus", "Subtract"]
1027 this.code_nativeKey = {};
1029 for (let list in values(this.keyTable))
1030 for (let v in values(list)) {
1032 v = v.toLowerCase();
1033 this.key_key[v.toLowerCase()] = v;
1036 for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1037 if (!/^DOM_VK_/.test(k))
1040 this.code_nativeKey[v] = k.substr(4);
1042 k = k.substr(7).toLowerCase();
1043 let names = [k.replace(/(^|_)(.)/g, (m, n1, n2) => n2.toUpperCase())
1044 .replace(/^NUMPAD/, "k")];
1046 if (names[0].length == 1)
1047 names[0] = names[0].toLowerCase();
1049 if (k in this.keyTable)
1050 names = this.keyTable[k];
1052 this.code_key[v] = names[0];
1053 for (let [, name] in Iterator(names)) {
1054 this.key_key[name.toLowerCase()] = name;
1055 this.key_code[name.toLowerCase()] = v;
1059 // HACK: as Gecko does not include an event for <, we must add this in manually.
1060 if (!("<" in this.key_code)) {
1061 this.key_code["<"] = 60;
1062 this.key_code["lt"] = 60;
1063 this.code_key[60] = "lt";
1069 code_key: Class.Memoize(function (prop) this.init()[prop]),
1070 code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
1071 keyTable: Class.Memoize(function (prop) this.init()[prop]),
1072 key_code: Class.Memoize(function (prop) this.init()[prop]),
1073 key_key: Class.Memoize(function (prop) this.init()[prop]),
1074 pseudoKeys: Set(["count", "leader", "nop", "pass"]),
1077 * Converts a user-input string of keys into a canonical
1080 * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
1081 * <C- > maps to <C-Space>, <S-a> maps to A
1082 * << maps to <lt><lt>
1084 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1087 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1090 * @param {string} keys Messy form.
1091 * @param {boolean} unknownOk Whether unknown keys are passed
1092 * through rather than being converted to <lt>keyname>.
1094 * @returns {string} Canonical form.
1096 canonicalKeys: function canonicalKeys(keys, unknownOk=true) {
1097 return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1100 iterKeys: function iterKeys(keys) iter(function () {
1101 let match, re = /<.*?>?>|[^<]/g;
1102 while (match = re.exec(keys))
1107 * Converts an event string into an array of pseudo-event objects.
1109 * These objects can be used as arguments to {@link #stringify} or
1110 * {@link DOM.Event}, though they are unlikely to be much use for other
1111 * purposes. They have many of the properties you'd expect to find on a
1112 * real event, but none of the methods.
1114 * Also may contain two "special" parameters, .dactylString and
1115 * .dactylShift these are set for characters that can never by
1116 * typed, but may appear in mappings, for example <Nop> is passed as
1117 * dactylString, and dactylShift is set when a user specifies
1118 * <S-@> where @ is a non-case-changeable, non-space character.
1120 * @param {string} keys The string to parse.
1121 * @param {boolean} unknownOk Whether unknown keys are passed
1122 * through rather than being converted to <lt>keyname>.
1124 * @returns {Array[Object]}
1126 parse: function parse(input, unknownOk=true) {
1128 return array.flatten(input.map(k => this.parse(k, unknownOk)));
1131 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1132 let evt_str = match[0];
1134 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1135 keyCode: 0, charCode: 0, type: "keypress" };
1137 if (evt_str.length == 1) {
1138 evt_obj.charCode = evt_str.charCodeAt(0);
1139 evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1140 evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1143 let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1144 modifier = Set(modifier.toUpperCase());
1145 keyname = keyname.toLowerCase();
1146 evt_obj.dactylKeyname = keyname;
1147 if (/^u[0-9a-f]+$/.test(keyname))
1148 keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1150 if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1151 this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
1152 evt_obj.globKey ="*" in modifier;
1153 evt_obj.ctrlKey ="C" in modifier;
1154 evt_obj.altKey ="A" in modifier;
1155 evt_obj.shiftKey ="S" in modifier;
1156 evt_obj.metaKey ="M" in modifier || "⌘" in modifier;
1157 evt_obj.dactylShift = evt_obj.shiftKey;
1159 if (keyname.length == 1) { // normal characters
1160 if (evt_obj.shiftKey)
1161 keyname = keyname.toUpperCase();
1163 evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1164 evt_obj.charCode = keyname.charCodeAt(0);
1165 evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1167 else if (Set.has(this.pseudoKeys, keyname)) {
1168 evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1170 else if (/mouse$/.test(keyname)) { // mouse events
1171 evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
1172 evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1173 delete evt_obj.keyCode;
1174 delete evt_obj.charCode;
1176 else { // spaces, control characters, and <
1177 evt_obj.keyCode = this.key_code[keyname];
1178 evt_obj.charCode = 0;
1181 else { // an invalid sequence starting with <, treat as a literal
1182 out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1187 // TODO: make a list of characters that need keyCode and charCode somewhere
1188 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1189 evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1190 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1191 evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1193 evt_obj.modifiers = (evt_obj.ctrlKey && Ci.nsIDOMNSEvent.CONTROL_MASK)
1194 | (evt_obj.altKey && Ci.nsIDOMNSEvent.ALT_MASK)
1195 | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1196 | (evt_obj.metaKey && Ci.nsIDOMNSEvent.META_MASK);
1204 * Converts the specified event to a string in dactyl key-code
1205 * notation. Returns null for an unknown event.
1207 * @param {Event} event
1210 stringify: function stringify(event) {
1212 return event.map(e => this.stringify(e)).join("");
1214 if (event.dactylString)
1215 return event.dactylString;
1229 if (/^key/.test(event.type)) {
1230 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1231 if (charCode == 0) {
1232 if (event.keyCode in this.code_key) {
1233 key = this.code_key[event.keyCode];
1235 if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1236 || event.ctrlKey || event.altKey || event.metaKey)
1237 || event.dactylShift)
1239 else if (!modifier && key.length === 1)
1241 key = key.toUpperCase();
1243 key = key.toLowerCase();
1245 if (!modifier && key.length == 1)
1249 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1250 // (i.e., cntrl codes 27--31)
1252 // For more information, see:
1253 // [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1254 // [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1255 // https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1256 // [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1257 // https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1260 // The following fixes are only activated if config.OS.isMacOSX.
1261 // Technically, they prevent mappings from <C-Esc> (and
1262 // <C-C-]> if your fancy keyboard permits such things<?>), but
1263 // these <C-control> mappings are probably pathological (<C-Esc>
1264 // certainly is on Windows), and so it is probably
1265 // harmless to remove the config.OS.isMacOSX if desired.
1267 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1268 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1270 modifier = modifier.replace("C-", "");
1272 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1273 key = String.fromCharCode(charCode + 64);
1275 // a normal key like a, b, c, 0, etc.
1276 else if (charCode) {
1277 key = String.fromCharCode(charCode);
1279 if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1280 // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1281 if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1284 key = this.code_key[this.key_code[key]];
1287 // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1288 // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1289 if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1291 if (/^\s$/.test(key))
1292 key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1293 else if (modifier.length == 0)
1300 key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1305 else if (event.type == "click" || event.type == "dblclick") {
1308 if (event.type == "dblclick")
1310 // TODO: triple and quadruple click
1312 switch (event.button) {
1317 key = "MiddleMouse";
1328 return "<" + modifier + key + ">";
1332 load: { bubbles: false },
1333 submit: { cancelable: true }
1336 types: Class.Memoize(() => iter(
1338 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1340 "popupshowing popupshown popuphiding popuphidden " +
1342 Key: "keydown keypress keyup",
1343 "": "change command dactyl-input input submit " +
1344 "load unload pageshow pagehide DOMContentLoaded " +
1347 ).map(([k, v]) => v.split(" ").map(v => [v, k]))
1352 * Dispatches an event to an element as if it were a native event.
1354 * @param {Node} target The DOM node to which to dispatch the event.
1355 * @param {Event} event The event to dispatch.
1357 dispatch: Class.Memoize(function ()
1358 config.haveGecko("2b")
1359 ? function dispatch(target, event, extra) {
1361 this.feedingEvent = extra;
1363 if (target instanceof Ci.nsIDOMElement)
1364 // This causes a crash on Gecko<2.0, it seems.
1365 return (target.ownerDocument || target.document || target).defaultView
1366 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1367 .dispatchDOMEventViaPresShell(target, event, true);
1369 target.dispatchEvent(event);
1370 return !event.defaultPrevented;
1374 util.reportError(e);
1377 this.feedingEvent = null;
1380 : function dispatch(target, event, extra) {
1382 this.feedingEvent = extra;
1383 target.dispatchEvent(update(event, extra));
1386 this.feedingEvent = null;
1391 createContents: Class.Memoize(() => services.has("dactyl") && services.dactyl.createContents
1394 isScrollable: Class.Memoize(() => services.has("dactyl") && services.dactyl.getScrollable
1395 ? (elem, dir) => services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1396 : (elem, dir) => true),
1398 isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1399 || isObject(val) && "toDOM" in val,
1401 DOMString: function DOMString(val) ({
1402 __proto__: DOMString.prototype,
1404 toDOM: function toDOM(doc) doc.createTextNode(val),
1406 toString: function () val
1410 * The set of input element type attribute values that mark the element as
1411 * an editable field.
1413 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1414 "month", "number", "password", "range", "search",
1415 "tel", "text", "time", "url", "week"]),
1418 * Converts a given DOM Node, Range, or Selection to a string. If
1419 * *html* is true, the output is HTML, otherwise it is presentation
1422 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1424 * @param {boolean} html Whether the output should be HTML rather
1425 * than presentation text.
1427 stringify: function stringify(node, html) {
1428 if (node instanceof Ci.nsISelection && node.isCollapsed)
1431 if (node instanceof Ci.nsIDOMNode) {
1432 let range = node.ownerDocument.createRange();
1433 range.selectNode(node);
1436 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1437 doc = doc.ownerDocument || doc;
1439 let encoder = services.HtmlEncoder();
1440 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1441 if (node instanceof Ci.nsISelection)
1442 encoder.setSelection(node);
1443 else if (node instanceof Ci.nsIDOMRange)
1444 encoder.setRange(node);
1446 let str = services.String(encoder.encodeToString());
1450 let [result, length] = [{}, {}];
1451 services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1452 return result.value.QueryInterface(Ci.nsISupportsString).data;
1456 * Compiles a CSS spec and XPath pattern matcher based on the given
1457 * list. List elements prefixed with "xpath:" are parsed as XPath
1458 * patterns, while other elements are parsed as CSS specs. The
1459 * returned function will, given a node, return an iterator of all
1460 * descendants of that node which match the given specs.
1462 * @param {[string]} list The list of patterns to match.
1463 * @returns {function(Node)}
1465 compileMatcher: function compileMatcher(list) {
1466 let xpath = [], css = [];
1467 for (let elem in values(list))
1468 if (/^xpath:/.test(elem))
1469 xpath.push(elem.substr(6));
1474 function matcher(node) {
1476 for (let elem in DOM.XPath(matcher.xpath, node))
1480 for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1483 css: css.join(", "),
1484 xpath: xpath.join(" | ")
1489 * Validates a list as input for {@link #compileMatcher}. Returns
1490 * true if and only if every element of the list is a valid XPath or
1493 * @param {[string]} list The list of patterns to test
1494 * @returns {boolean} True when the patterns are all valid.
1496 validateMatcher: function validateMatcher(list) {
1497 return this.testValues(list, DOM.closure.testMatcher);
1500 testMatcher: function testMatcher(value) {
1501 let evaluator = services.XPathEvaluator();
1502 let node = services.XMLDocument();
1503 if (/^xpath:/.test(value))
1504 util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1506 util.withProperErrors("querySelector", node, value);
1511 * Converts HTML special characters in *str* to the equivalent HTML
1514 * @param {string} str
1515 * @param {boolean} simple If true, only escape for the simple case
1519 escapeHTML: function escapeHTML(str, simple) {
1520 let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
1521 let regexp = simple ? /[<>]/g : /['"&<>]/g;
1522 return str.replace(regexp, m => map[m]);
1526 * Converts an E4X XML literal to a DOM node. Any attribute named
1527 * highlight is present, it is transformed into dactyl:highlight,
1528 * and the named highlight groups are guaranteed to be loaded.
1530 * @param {Node} node
1531 * @param {Document} doc
1532 * @param {Object} nodes If present, nodes with the "key" attribute are
1533 * stored here, keyed to the value thereof.
1536 fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
1537 prefs.get("javascript.options.xml.chrome") !== false
1538 && require("dom-e4x").fromXML }),
1540 fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1544 function tag(args, namespaces) {
1545 let _namespaces = namespaces;
1547 // Deal with common error case
1549 util.reportError(Error("Unexpected null when processing XML."));
1550 args = ["html:i", {}, "[NULL]"];
1553 if (isinstance(args, ["String", "Number", "Boolean", _]))
1554 return doc.createTextNode(args);
1556 return DOM.fromXML(args, doc, nodes);
1557 if (isObject(args) && "toDOM" in args)
1558 return args.toDOM(doc, namespaces, nodes);
1559 if (args instanceof Ci.nsIDOMNode)
1561 if (args instanceof DOM)
1562 return args.fragment();
1563 if ("toJSONXML" in args)
1564 args = args.toJSONXML();
1566 let [name, attr] = args;
1568 if (!isString(name) || args.length == 0 || name === "") {
1569 var frag = doc.createDocumentFragment();
1570 Array.forEach(args, function (arg) {
1571 if (!isArray(arg[0]))
1573 arg.forEach(function (arg) {
1574 frag.appendChild(tag(arg, namespaces));
1582 function parseNamespace(name) DOM.parseNamespace(name, namespaces);
1584 // FIXME: Surely we can do better.
1585 for (var key in attr) {
1586 if (/^xmlns(?:$|:)/.test(key)) {
1587 if (_namespaces === namespaces)
1588 namespaces = Object.create(namespaces);
1590 namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1593 var args = Array.slice(args, 2);
1594 var vals = parseNamespace(name);
1595 var elem = doc.createElementNS(vals[0] || namespaces[""],
1598 for (var key in attr)
1599 if (!/^xmlns(?:$|:)/.test(key)) {
1600 var val = attr[key];
1601 if (nodes && key == "key")
1604 vals = parseNamespace(key);
1605 if (key == "highlight")
1607 else if (typeof val == "function")
1608 elem.addEventListener(key.replace(/^on/, ""), val, false);
1610 elem.setAttributeNS(vals[0] || "", key, val);
1612 args.forEach(function (e) {
1613 elem.appendChild(tag(e, namespaces));
1616 if ("highlight" in attr)
1617 highlight.highlightNode(elem, attr.highlight, nodes || true);
1622 namespaces = update({}, fromJSON.namespaces, namespaces);
1624 namespaces = fromJSON.namespaces;
1626 return tag(xml, namespaces);
1629 "": "http://www.w3.org/1999/xhtml",
1631 html: "http://www.w3.org/1999/xhtml",
1632 xmlns: "http://www.w3.org/2000/xmlns/",
1633 xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
1637 toXML: function toXML(xml) {
1639 let doc = services.XMLDocument();
1640 let node = this.fromJSON(xml, doc);
1641 return services.XMLSerializer()
1642 .serializeToString(node);
1645 toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1646 const INDENT = indent || " ";
1648 const EMPTY = Set("area base basefont br col frame hr img input isindex link meta param"
1651 function namespaced(namespaces, namespace, localName) {
1652 for (let [k, v] in Iterator(namespaces))
1654 return (k ? k + ":" + localName : localName);
1656 throw Error("No such namespace");
1659 function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1661 function hasString(args) {
1662 return args.some(a => (isString(a) || isFragment(a) && hasString(a)));
1665 function isStrings(args) {
1667 return util.dump("ARGS: " + {}.toString.call(args) + " " + args), false;
1668 return args.every(a => (isinstance(a, ["String", DOM.DOMString]) || isFragment(a) && isStrings(a)));
1671 function tag(args, namespaces, indent) {
1672 let _namespaces = namespaces;
1677 if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1679 DOM.escapeHTML(String(args), true);
1684 .replace(/^/m, indent);
1686 if (isObject(args) && "toDOM" in args)
1688 services.XMLSerializer()
1689 .serializeToString(args.toDOM(services.XMLDocument()))
1690 .replace(/^/m, indent);
1692 if (args instanceof Ci.nsIDOMNode)
1694 services.XMLSerializer()
1695 .serializeToString(args)
1696 .replace(/^/m, indent);
1698 if ("toJSONXML" in args)
1699 args = args.toJSONXML();
1701 // Deal with common error case
1703 util.reportError(Error("Unexpected null when processing XML."));
1707 let [name, attr] = args;
1709 if (isFragment(args)) {
1711 let join = isArray(args) && isStrings(args) ? "" : "\n";
1712 Array.forEach(args, function (arg) {
1713 if (!isArray(arg[0]))
1717 arg.forEach(function (arg) {
1718 let string = tag(arg, namespaces, indent);
1720 contents.push(string);
1722 if (contents.length)
1723 res.push(contents.join("\n"), join);
1725 if (res[res.length - 1] == join)
1727 return res.join("");
1732 function parseNamespace(name) {
1733 var m = /^(?:(.*):)?(.*)$/.exec(name);
1734 return [namespaces[m[1]], m[2]];
1737 // FIXME: Surely we can do better.
1739 for (var key in attr) {
1740 if (/^xmlns(?:$|:)/.test(key)) {
1741 if (_namespaces === namespaces)
1742 namespaces = update({}, namespaces);
1744 let ns = namespaces[attr[key]] || attr[key];
1745 if (ns == namespaces[key.substr(6)])
1746 skipAttr[key] = true;
1748 attr[key] = namespaces[key.substr(6)] = ns;
1751 var args = Array.slice(args, 2);
1752 var vals = parseNamespace(name);
1754 let res = [indent, "<", name];
1756 for (let [key, val] in Iterator(attr)) {
1757 if (Set.has(skipAttr, key))
1760 let vals = parseNamespace(key);
1761 if (typeof val == "function") {
1762 key = key.replace(/^(?:on)?/, "on");
1763 val = val.toSource() + "(event)";
1766 if (key != "highlight" || vals[0] == String(NS))
1767 res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1769 res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1770 '="', DOM.escapeHTML(val), '"');
1773 if ((vals[0] || namespaces[""]) == String(XHTML) && Set.has(EMPTY, vals[1])
1774 || asXML && !args.length)
1779 if (isStrings(args))
1780 res.push(args.map(e => tag(e, namespaces, "")).join(""),
1784 args.forEach(function (e) {
1785 let string = tag(e, namespaces, indent + INDENT);
1787 contents.push(string);
1790 res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1794 return res.join("");
1798 namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1800 namespaces = DOM.fromJSON.namespaces;
1802 return tag(xml, namespaces, "");
1805 parseNamespace: function parseNamespace(name, namespaces) {
1806 if (name == "xmlns")
1807 return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1809 var m = /^(?:(.*):)?(.*)$/.exec(name);
1810 return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
1815 * Evaluates an XPath expression in the current or provided
1816 * document. It provides the xhtml, xhtml2 and dactyl XML
1817 * namespaces. The result may be used as an iterator.
1819 * @param {string} expression The XPath expression to evaluate.
1820 * @param {Node} elem The context element.
1821 * @param {boolean} asIterator Whether to return the results as an
1823 * @param {object} namespaces Additional namespaces to recognize.
1825 * @returns {Object} Iterable result of the evaluation.
1828 function XPath(expression, elem, asIterator, namespaces) {
1830 let doc = elem.ownerDocument || elem;
1832 if (isArray(expression))
1833 expression = DOM.makeXPath(expression);
1835 let resolver = XPath.resolver;
1837 namespaces = update({}, DOM.namespaces, namespaces);
1838 resolver = prefix => namespaces[prefix] || null;
1841 let result = doc.evaluate(expression, elem,
1843 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1848 iterateNext: function () result.iterateNext(),
1849 get resultType() result.resultType,
1850 get snapshotLength() result.snapshotLength,
1851 snapshotItem: function (i) result.snapshotItem(i),
1853 asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1854 : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1859 throw e.stack ? e : Error(e);
1863 resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1867 * Returns an XPath union expression constructed from the specified node
1868 * tests. An expression is built with node tests for both the null and
1869 * XHTML namespaces. See {@link DOM.XPath}.
1871 * @param nodes {Array(string)}
1874 makeXPath: function makeXPath(nodes) {
1875 return array(nodes).map(util.debrace).flatten()
1876 .map(node => /^[a-z]+:/.test(node) ? node
1877 : [node, "xhtml:" + node])
1879 .map(node => "//" + node).join(" | ");
1886 xhtml2: "http://www.w3.org/2002/06/xhtml2",
1890 namespaceNames: Class.Memoize(function ()
1891 iter(this.namespaces).map(([k, v]) => ([v, k])).toObject()),
1894 Object.keys(DOM.Event.types).forEach(function (event) {
1895 let name = event.replace(/-(.)/g, (m, m1) => m1.toUpperCase());
1896 if (!Set.has(DOM.prototype, name))
1897 DOM.prototype[name] =
1898 function _event(arg, extra) {
1899 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1907 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1909 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: