]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/dom.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / dom.jsm
1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2014 Kris Maglione <maglione.k@gmail.com>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 "use strict";
7
8 defineModule("dom", {
9     exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
10 });
11
12 lazyRequire("highlight", ["highlight"]);
13 lazyRequire("messages", ["_"]);
14 lazyRequire("overlay", ["overlay"]);
15 lazyRequire("prefs", ["prefs"]);
16 lazyRequire("template", ["template"]);
17
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";
22
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);
28         else
29             elem.setAttribute(attr, true);
30     }
31 });
32
33 /**
34  * @class
35  *
36  * A jQuery-inspired DOM utility framework.
37  *
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.
41  */
42 var DOM = Class("DOM", {
43     init: function init(val, context, nodes) {
44         let self;
45         let length = 0;
46
47         if (nodes)
48             this.nodes = nodes;
49
50         if (context instanceof Ci.nsIDOMDocument)
51             this.document = context;
52
53         if (typeof val == "string")
54             val = context.querySelectorAll(val);
55
56         if (val == null)
57             ;
58         else if (DOM.isJSONXML(val)) {
59             if (context instanceof Ci.nsIDOMDocument)
60                 this[length++] = DOM.fromJSON(val, context, this.nodes);
61             else
62                 this[length++] = val;
63         }
64         else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
65             this[length++] = val;
66         else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
67             for (let elem in val)
68                 this[length++] = elem;
69         else if ("length" in val)
70             for (let i = 0; i < val.length; i++)
71                 this[length++] = val[i];
72         else
73             this[length++] = val;
74
75         this.length = length;
76         return self || this;
77     },
78
79     __iterator__: function __iterator__() {
80         for (let i = 0; i < this.length; i++)
81             yield this[i];
82     },
83
84     Empty: function Empty() this.constructor(null, this.document),
85
86     nodes: Class.Memoize(function () ({})),
87
88     get items() {
89         for (let i = 0; i < this.length; i++)
90             yield this.eq(i);
91     },
92
93     get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
94     set document(val) this._document = val,
95
96     attrHooks: array.toObject([
97         ["", {
98             href: { get: function (elem) elem.href || elem.getAttribute("href") },
99             src:  { get: function (elem) elem.src || elem.getAttribute("src") },
100             checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
101                        set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val; } },
102             collapsed: BooleanAttribute("collapsed"),
103             disabled: BooleanAttribute("disabled"),
104             hidden: BooleanAttribute("hidden"),
105             readonly: BooleanAttribute("readonly")
106         }]
107     ]),
108
109     matcher: function matcher(sel) elem => (elem.mozMatchesSelector && elem.mozMatchesSelector(sel)),
110
111     each: function each(fn, self) {
112         let obj = self || this.Empty();
113         for (let i = 0; i < this.length; i++)
114             fn.call(self || update(obj, [this[i]]), this[i], i);
115         return this;
116     },
117
118     eachDOM: function eachDOM(val, fn, self) {
119         let dom = this;
120         function munge(val, container, idx) {
121             if (val instanceof Ci.nsIDOMRange)
122                 return val.extractContents();
123             if (val instanceof Ci.nsIDOMNode)
124                 return val;
125
126             if (DOM.isJSONXML(val)) {
127                 val = dom.constructor(val, dom.document);
128                 if (container)
129                     container[idx] = val[0];
130             }
131
132             if (isObject(val) && "length" in val) {
133                 let frag = dom.document.createDocumentFragment();
134                 for (let i = 0; i < val.length; i++)
135                     frag.appendChild(munge(val[i], val, i));
136                 return frag;
137             }
138             return val;
139         }
140
141         if (DOM.isJSONXML(val))
142             val = (function () this).bind(val);
143
144         if (callable(val))
145             return this.each(function (elem, i) {
146                 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
147             }, self || this);
148
149         if (this.length)
150             util.withProperErrors(fn, self || this, munge(val), this[0], 0);
151         return this;
152     },
153
154     eq: function eq(idx) {
155         return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
156     },
157
158     find: function find(val) {
159         return this.map(elem => elem.querySelectorAll(val));
160     },
161
162     findAnon: function findAnon(attr, val) {
163         return this.map(elem => elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
164     },
165
166     filter: function filter(val, self) {
167         let res = this.Empty();
168
169         if (!callable(val))
170             val = this.matcher(val);
171
172         this.constructor(Array.filter(this, val, self || this));
173         let obj = self || this.Empty();
174         for (let i = 0; i < this.length; i++)
175             if (val.call(self || update(obj, [this[i]]), this[i], i))
176                 res[res.length++] = this[i];
177
178         return res;
179     },
180
181     is: function is(val) {
182         return this.some(this.matcher(val));
183     },
184
185     reverse: function reverse() {
186         Array.reverse(this);
187         return this;
188     },
189
190     all: function all(fn, self) {
191         let res = this.Empty();
192
193         this.each(function (elem) {
194             while (true) {
195                 elem = fn.call(this, elem);
196                 if (elem instanceof Ci.nsIDOMNode)
197                     res[res.length++] = elem;
198                 else if (elem && "length" in elem)
199                     for (let i = 0; i < elem.length; i++)
200                         res[res.length++] = elem[j];
201                 else
202                     break;
203             }
204         }, self || this);
205         return res;
206     },
207
208     map: function map(fn, self) {
209         let res = this.Empty();
210         let obj = self || this.Empty();
211
212         for (let i = 0; i < this.length; i++) {
213             let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
214             if (isObject(tmp) && !(tmp instanceof Ci.nsIDOMNode) && "length" in tmp)
215                 for (let j = 0; j < tmp.length; j++)
216                     res[res.length++] = tmp[j];
217             else if (tmp != null)
218                 res[res.length++] = tmp;
219         }
220
221         return res;
222     },
223
224     slice: function eq(start, end) {
225         return this.constructor(Array.slice(this, start, end));
226     },
227
228     some: function some(fn, self) {
229         for (let i = 0; i < this.length; i++)
230             if (fn.call(self || this, this[i], i))
231                 return true;
232         return false;
233     },
234
235     get parent() this.map(elem => elem.parentNode, this),
236
237     get offsetParent() this.map(function (elem) {
238         do {
239             var parent = elem.offsetParent;
240             if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
241                 return parent;
242         }
243         while (parent);
244     }, this),
245
246     get ancestors() this.all(elem => elem.parentNode),
247
248     get children() this.map(elem => Array.filter(elem.childNodes,
249                                                  e => e instanceof Ci.nsIDOMElement),
250                             this),
251
252     get contents() this.map(elem => elem.childNodes, this),
253
254     get siblings() this.map(elem => Array.filter(elem.parentNode.childNodes,
255                                                  e => e != elem && e instanceof Ci.nsIDOMElement),
256                             this),
257
258     get siblingsBefore() this.all(elem => elem.previousElementSibling),
259     get siblingsAfter() this.all(elem => elem.nextElementSibling),
260
261     get allSiblingsBefore() this.all(elem => elem.previousSibling),
262     get allSiblingsAfter() this.all(elem => elem.nextSibling),
263
264     get class() let (self = this) ({
265         toString: function () self[0].className,
266
267         get list() Array.slice(self[0].classList),
268         set list(val) self.attr("class", val.join(" ")),
269
270         each: function each(meth, arg) {
271             return self.each(function (elem) {
272                 elem.classList[meth](arg);
273             });
274         },
275
276         add: function add(cls) this.each("add", cls),
277         remove: function remove(cls) this.each("remove", cls),
278         toggle: function toggle(cls, val, thisObj) {
279             if (callable(val))
280                 return self.each(function (elem, i) {
281                     this.class.toggle(cls, val.call(thisObj || this, elem, i));
282                 });
283             return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
284         },
285
286         has: function has(cls) this[0].classList.has(cls)
287     }),
288
289     get highlight() let (self = this) ({
290         toString: function () self.attrNS(NS, "highlight") || "",
291
292         get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
293         set list(val) {
294             let str = array.uniq(val).join(" ").trim();
295             self.attrNS(NS, "highlight", str || null);
296         },
297
298         has: function has(hl) ~this.list.indexOf(hl),
299
300         add: function add(hl) self.each(function () {
301             highlight.loaded[hl] = true;
302             this.highlight.list = this.highlight.list.concat(hl);
303         }),
304
305         remove: function remove(hl) self.each(function () {
306             this.highlight.list = this.highlight.list.filter(h => h != hl);
307         }),
308
309         toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
310             let { highlight } = this;
311             let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
312
313             highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl);
314         }),
315     }),
316
317     get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
318                                                       height: this[0].scrollMaxY + this[0].innerHeight,
319                                                       get right() this.width + this.left,
320                                                       get bottom() this.height + this.top,
321                                                       top: -this[0].scrollY,
322                                                       left: -this[0].scrollX } :
323                this[0]                            ? this[0].getBoundingClientRect() : {},
324
325     get viewport() {
326         let node = this[0];
327         if (node instanceof Ci.nsIDOMDocument)
328             node = node.defaultView;
329
330         if (node instanceof Ci.nsIDOMWindow)
331             return {
332                 get width() this.right - this.left,
333                 get height() this.bottom - this.top,
334                 bottom: node.innerHeight,
335                 right: node.innerWidth,
336                 top: 0, left: 0
337             };
338
339         let r = this.rect;
340         return {
341             width: node.clientWidth,
342             height: node.clientHeight,
343             top: r.top + node.clientTop,
344             get bottom() this.top + this.height,
345             left: r.left + node.clientLeft,
346             get right() this.left + this.width
347         };
348     },
349
350     scrollPos: function scrollPos(left, top) {
351         if (arguments.length == 0) {
352             if (this[0] instanceof Ci.nsIDOMElement)
353                 return { top: this[0].scrollTop, left: this[0].scrollLeft,
354                          height: this[0].scrollHeight, width: this[0].scrollWidth,
355                          innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
356
357             if (this[0] instanceof Ci.nsIDOMWindow)
358                 return { top: this[0].scrollY, left: this[0].scrollX,
359                          height: this[0].scrollMaxY + this[0].innerHeight,
360                          width: this[0].scrollMaxX + this[0].innerWidth,
361                          innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
362
363             return null;
364         }
365         let func = callable(left) && left;
366
367         return this.each(function (elem, i) {
368             if (func)
369                 ({ left, top }) = func.call(this, elem, i);
370
371             if (elem instanceof Ci.nsIDOMWindow)
372                 elem.scrollTo(left == null ? elem.scrollX : left,
373                               top  == null ? elem.scrollY : top);
374             else {
375                 if (left != null)
376                     elem.scrollLeft = left;
377                 if (top != null)
378                     elem.scrollTop = top;
379             }
380         });
381     },
382
383     /**
384      * Returns true if the given DOM node is currently visible.
385      * @returns {boolean}
386      */
387     get isVisible() {
388         let style = this[0] && this.style;
389         return style && style.visibility == "visible" && style.display != "none";
390     },
391
392     get editor() {
393         if (!this.length)
394             return;
395
396         this[0] instanceof Ci.nsIDOMNSEditableElement;
397         try {
398             if (this[0].editor instanceof Ci.nsIEditor)
399                 var editor = this[0].editor;
400         }
401         catch (e) {
402             util.reportError(e);
403         }
404
405         try {
406             if (!editor)
407                 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
408                                 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
409                                 .getEditorForWindow(this[0]);
410         }
411         catch (e) {}
412
413         editor instanceof Ci.nsIPlaintextEditor;
414         editor instanceof Ci.nsIHTMLEditor;
415         return editor;
416     },
417
418     get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
419
420     get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
421                                        Ci.nsIDOMHTMLTextAreaElement,
422                                        Ci.nsIDOMXULTextBoxElement])
423                     && this.isEditable,
424
425     /**
426      * Returns an object representing a Node's computed CSS style.
427      * @returns {Object}
428      */
429     get style() {
430         let node = this[0];
431         if (node instanceof Ci.nsIDOMWindow)
432             node = node.document;
433         if (node instanceof Ci.nsIDOMDocument)
434             node = node.documentElement;
435         while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
436             node = node.parentNode;
437
438         try {
439             var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
440         }
441         catch (e) {}
442
443         if (res == null) {
444             util.dumpStack(_("error.nullComputedStyle", node));
445             Cu.reportError(Error(_("error.nullComputedStyle", node)));
446             return {};
447         }
448         return res;
449     },
450
451     /**
452      * Parses the fields of a form and returns a URL/POST-data pair
453      * that is the equivalent of submitting the form.
454      *
455      * @returns {object} An object with the following elements:
456      *      url: The URL the form points to.
457      *      postData: A string containing URL-encoded post data, if this
458      *                form is to be POSTed
459      *      charset: The character set of the GET or POST data.
460      *      elements: The key=value pairs used to generate query information.
461      */
462     // Nuances gleaned from browser.jar/content/browser/browser.js
463     get formData() {
464         function encode(name, value, param) {
465             param = param ? "%s" : "";
466             if (post)
467                 return name + "=" + encodeComponent(value + param);
468             return encodeComponent(name) + "=" + encodeComponent(value) + param;
469         }
470
471         let field = this[0];
472         let form = field.form;
473         let doc = form.ownerDocument;
474
475         let charset = doc.characterSet;
476         let converter = services.CharsetConv(charset);
477         for (let cs of form.acceptCharset.split(/\s*,\s*|\s+/)) {
478             let c = services.CharsetConv(cs);
479             if (c) {
480                 converter = services.CharsetConv(cs);
481                 charset = cs;
482             }
483         }
484
485         let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
486         let url = util.newURI(form.action, charset, uri).spec;
487
488         let post = form.method.toUpperCase() == "POST";
489
490         let encodeComponent = encodeURIComponent;
491         if (charset !== "UTF-8")
492             encodeComponent = function encodeComponent(str)
493                 escape(converter.ConvertFromUnicode(str) + converter.Finish());
494
495         let elems = [];
496         if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
497             elems.push(encode(field.name, field.value));
498
499         for (let [, elem] in iter(form.elements))
500             if (elem.name && !elem.disabled) {
501                 if (DOM(elem).isInput
502                         || /^(?:hidden|textarea)$/.test(elem.type)
503                         || elem.type == "submit" && elem == field
504                         || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) {
505
506                     if (elem !== field)
507                         elems.push(encode(elem.name, elem.value));
508                     else if (overlay.getData(elem, "had-focus"))
509                         elems.push(encode(elem.name, elem.value, true));
510                     else
511                         elems.push(encode(elem.name, "", true));
512                 }
513                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
514                     for (let [, opt] in Iterator(elem.options))
515                         if (opt.selected)
516                             elems.push(encode(elem.name, opt.value));
517                 }
518             }
519
520         if (post)
521             return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
522         return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
523     },
524
525     /**
526      * Generates an XPath expression for the given element.
527      *
528      * @returns {string}
529      */
530     get xpath() {
531         function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
532         if (!(this[0] instanceof Ci.nsIDOMElement))
533             return null;
534
535         let res = [];
536         let doc = this.document;
537         for (let elem = this[0];; elem = elem.parentNode) {
538             if (!(elem instanceof Ci.nsIDOMElement))
539                 res.push("");
540             else if (elem.id)
541                 res.push("id(" + quote(elem.id) + ")");
542             else {
543                 let name = elem.localName;
544                 if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
545                     if (elem.namespaceURI in DOM.namespaceNames)
546                         name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
547                     else
548                         name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
549
550                 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
551                 continue;
552             }
553             break;
554         }
555
556         return res.reverse().join("/");
557     },
558
559     /**
560      * Returns a string or XML representation of this node.
561      *
562      * @param {boolean} color If true, return a colored, XML
563      *  representation of this node.
564      */
565     repr: function repr(color) {
566         function namespaced(node) {
567             var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
568             if (!ns)
569                 return node.localName;
570             if (color)
571                 return [["span", { highlight: "HelpXMLNamespace" }, ns],
572                         node.localName];
573             return ns + ":" + node.localName;
574         }
575
576         let res = [];
577         this.each(function (elem) {
578             try {
579                 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling);
580                 if (color)
581                     res.push(["span", { highlight: "HelpXML" },
582                         ["span", { highlight: "HelpXMLTagStart" },
583                             "<", namespaced(elem), " ",
584                             template.map(array.iterValues(elem.attributes),
585                                 attr => [
586                                     ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
587                                     ["span", { highlight: "HelpXMLString" }, attr.value]
588                                 ],
589                                 " "),
590                             !hasChildren ? "/>" : ">",
591                         ],
592                         !hasChildren ? "" :
593                             ["", "...",
594                              ["span", { highlight: "HtmlTagEnd" }, "<", namespaced(elem), ">"]]
595                     ]);
596                 else {
597                     let tag = "<" + [namespaced(elem)].concat(
598                         [namespaced(a) + '="' + String.replace(a.value, /["<]/, DOM.escapeHTML) + '"'
599                          for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
600
601                     res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
602                 }
603             }
604             catch (e) {
605                 res.push({}.toString.call(elem));
606             }
607         }, this);
608         res = template.map(res, util.identity, ",");
609         return color ? res : res.join("");
610     },
611
612     attr: function attr(key, val) {
613         return this.attrNS("", key, val);
614     },
615
616     attrNS: function attrNS(ns, key, val) {
617         if (val !== undefined)
618             key = array.toObject([[key, val]]);
619
620         let hooks = this.attrHooks[ns] || {};
621
622         if (isObject(key))
623             return this.each(function (elem, i) {
624                 for (let [k, v] in Iterator(key)) {
625                     if (callable(v))
626                         v = v.call(this, elem, i);
627
628                     if (hasOwnProperty(hooks, k) && hooks[k].set)
629                         hooks[k].set.call(this, elem, v, k);
630                     else if (v == null)
631                         elem.removeAttributeNS(ns, k);
632                     else
633                         elem.setAttributeNS(ns, k, v);
634                 }
635             });
636
637         if (!this.length)
638             return null;
639
640         if (hasOwnProperty(hooks, key) && hooks[key].get)
641             return hooks[key].get.call(this, this[0], key);
642
643         if (!this[0].hasAttributeNS(ns, key))
644             return null;
645
646         return this[0].getAttributeNS(ns, key);
647     },
648
649     css: update(function css(key, val) {
650         if (val !== undefined)
651             key = array.toObject([[key, val]]);
652
653         if (isObject(key))
654             return this.each(function (elem) {
655                 for (let [k, v] in Iterator(key))
656                     elem.style[css.property(k)] = v;
657             });
658
659         return this[0].style[css.property(key)];
660     }, {
661         name: function (property) property.replace(/[A-Z]/g, m0 => "-" + m0.toLowerCase()),
662
663         property: function (name) name.replace(/-(.)/g, (m0, m1) => m1.toUpperCase())
664     }),
665
666     append: function append(val) {
667         return this.eachDOM(val, function (elem, target) {
668             target.appendChild(elem);
669         });
670     },
671
672     prepend: function prepend(val) {
673         return this.eachDOM(val, function (elem, target) {
674             target.insertBefore(elem, target.firstChild);
675         });
676     },
677
678     before: function before(val) {
679         return this.eachDOM(val, function (elem, target) {
680             target.parentNode.insertBefore(elem, target);
681         });
682     },
683
684     after: function after(val) {
685         return this.eachDOM(val, function (elem, target) {
686             target.parentNode.insertBefore(elem, target.nextSibling);
687         });
688     },
689
690     appendTo: function appendTo(elem) {
691         if (!(elem instanceof this.constructor))
692             elem = this.constructor(elem, this.document);
693         elem.append(this);
694         return this;
695     },
696
697     prependTo: function prependTo(elem) {
698         if (!(elem instanceof this.constructor))
699             elem = this.constructor(elem, this.document);
700         elem.prepend(this);
701         return this;
702     },
703
704     insertBefore: function insertBefore(elem) {
705         if (!(elem instanceof this.constructor))
706             elem = this.constructor(elem, this.document);
707         elem.before(this);
708         return this;
709     },
710
711     insertAfter: function insertAfter(elem) {
712         if (!(elem instanceof this.constructor))
713             elem = this.constructor(elem, this.document);
714         elem.after(this);
715         return this;
716     },
717
718     remove: function remove() {
719         return this.each(function (elem) {
720             if (elem.parentNode)
721                 elem.parentNode.removeChild(elem);
722         }, this);
723     },
724
725     empty: function empty() {
726         return this.each(function (elem) {
727             while (elem.firstChild)
728                 elem.removeChild(elem.firstChild);
729         }, this);
730     },
731
732     fragment: function fragment() {
733         let frag = this.document.createDocumentFragment();
734         this.appendTo(frag);
735         return this;
736     },
737
738     clone: function clone(deep)
739         this.map(elem => elem.cloneNode(deep)),
740
741     toggle: function toggle(val, self) {
742         if (callable(val))
743             return this.each(function (elem, i) {
744                 this[val.call(self || this, elem, i) ? "show" : "hide"]();
745             });
746
747         if (arguments.length)
748             return this[val ? "show" : "hide"]();
749
750         let hidden = this.map(elem => elem.style.display == "none");
751         return this.each(function (elem, i) {
752             this[hidden[i] ? "show" : "hide"]();
753         });
754     },
755     hide: function hide() {
756         return this.each(function (elem) { elem.style.display = "none"; }, this);
757     },
758     show: function show() {
759         for (let i = 0; i < this.length; i++)
760             if (!this[i].dactylDefaultDisplay && this[i].style.display)
761                 this[i].style.display = "";
762
763         this.each(function (elem) {
764             if (!elem.dactylDefaultDisplay)
765                 elem.dactylDefaultDisplay = this.style.display;
766         });
767
768         return this.each(function (elem) {
769             elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
770         }, this);
771     },
772
773     createContents: function createContents()
774         this.each(DOM.createContents, this),
775
776     isScrollable: function isScrollable(direction)
777         this.length && DOM.isScrollable(this[0], direction),
778
779     getSet: function getSet(args, get, set) {
780         if (!args.length)
781             return this[0] && get.call(this, this[0]);
782
783         let [fn, self] = args;
784         if (!callable(fn))
785             fn = () => args[0];
786
787         return this.each(function (elem, i) {
788             set.call(this, elem, fn.call(self || this, elem, i));
789         }, this);
790     },
791
792     html: function html(txt, self) {
793         return this.getSet(arguments,
794                            elem => elem.innerHTML,
795                            util.wrapCallback((elem, val) => { elem.innerHTML = val; }));
796     },
797
798     text: function text(txt, self) {
799         return this.getSet(arguments,
800                            elem => elem.textContent,
801                            (elem, val) => { elem.textContent = val; });
802     },
803
804     val: function val(txt) {
805         return this.getSet(arguments,
806                            elem => elem.value,
807                            (elem, val) => { elem.value = val == null ? "" : val; });
808     },
809
810     listen: function listen(event, listener, capture) {
811         if (isObject(event))
812             capture = listener;
813         else
814             event = array.toObject([[event, listener]]);
815
816         for (let [evt, callback] in Iterator(event))
817             event[evt] = util.wrapCallback(callback, true);
818
819         return this.each(function (elem) {
820             for (let [evt, callback] in Iterator(event))
821                 elem.addEventListener(evt, callback, capture);
822         });
823     },
824     unlisten: function unlisten(event, listener, capture) {
825         if (isObject(event))
826             capture = listener;
827         else
828             event = array.toObject([[event, listener]]);
829
830         return this.each(function (elem) {
831             for (let [k, v] in Iterator(event))
832                 elem.removeEventListener(k, v.wrapper || v, capture);
833         });
834     },
835     once: function once(event, listener, capture) {
836         if (isObject(event))
837             capture = listener;
838         else
839             event = array.toObject([[event, listener]]);
840
841         for (let pair in Iterator(event)) {
842             let [evt, callback] = pair;
843             event[evt] = util.wrapCallback(function wrapper(event) {
844                 this.removeEventListener(evt, wrapper.wrapper, capture);
845                 return callback.apply(this, arguments);
846             }, true);
847         }
848
849         return this.each(function (elem) {
850             for (let [k, v] in Iterator(event))
851                 elem.addEventListener(k, v, capture);
852         });
853     },
854
855     dispatch: function dispatch(event, params, extraProps) {
856         this.canceled = false;
857         return this.each(function (elem) {
858             let evt = DOM.Event(this.document, event, params, elem);
859             if (!DOM.Event.dispatch(elem, evt, extraProps))
860                 this.canceled = true;
861         }, this);
862     },
863
864     focus: function focus(arg, extra) {
865         if (callable(arg))
866             return this.listen("focus", arg, extra);
867
868         let elem = this[0];
869         let flags = arg || services.focus.FLAG_BYMOUSE;
870         try {
871             if (elem instanceof Ci.nsIDOMDocument)
872                 elem = elem.defaultView;
873             if (elem instanceof Ci.nsIDOMElement)
874                 services.focus.setFocus(elem, flags);
875             else if (elem instanceof Ci.nsIDOMWindow) {
876                 services.focus.focusedWindow = elem;
877                 if (services.focus.focusedWindow != elem)
878                     services.focus.clearFocus(elem);
879             }
880         }
881         catch (e) {
882             util.dump(elem);
883             util.reportError(e);
884         }
885         return this;
886     },
887     blur: function blur(arg, extra) {
888         if (callable(arg))
889             return this.listen("blur", arg, extra);
890         return this.each(function (elem) { elem.blur(); }, this);
891     },
892
893     /**
894      * Scrolls an element into view if and only if it's not already
895      * fully visible.
896      */
897     scrollIntoView: function scrollIntoView(alignWithTop) {
898         return this.each(function (elem) {
899             function getAlignment(viewport) {
900                 if (alignWithTop !== undefined)
901                     return alignWithTop;
902                 if (rect.bottom < viewport.top)
903                     return true;
904                 if (rect.top > viewport.bottom)
905                     return false;
906                 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom);
907             }
908
909             let rect;
910             function fix(parent) {
911                 if (!(parent[0] instanceof Ci.nsIDOMWindow)
912                         && parent.style.overflow == "visible")
913                     return;
914
915                 ({ rect }) = DOM(elem);
916                 let { viewport } = parent;
917                 let isect = util.intersection(rect, viewport);
918
919                 if (isect.height < Math.min(viewport.height, rect.height)) {
920                     let { top } = parent.scrollPos();
921                     if (getAlignment(viewport))
922                         parent.scrollPos(null, top - (viewport.top - rect.top));
923                     else
924                         parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
925
926                 }
927             }
928
929             for (let parent in this.ancestors.items)
930                 fix(parent);
931
932             fix(DOM(this.document.defaultView));
933         });
934     },
935 }, {
936     /**
937      * Creates an actual event from a pseudo-event object.
938      *
939      * The pseudo-event object (such as may be retrieved from
940      * DOM.Event.parse) should have any properties you want the event to
941      * have.
942      *
943      * @param {Document} doc The DOM document to associate this event with
944      * @param {Type} type The type of event (keypress, click, etc.)
945      * @param {Object} opts The pseudo-event. @optional
946      */
947     Event: Class("Event", {
948         init: function Event(doc, type, opts, target) {
949             const DEFAULTS = {
950                 HTML: {
951                     type: type, bubbles: true, cancelable: false
952                 },
953                 Key: {
954                     type: type,
955                     bubbles: true, cancelable: true,
956                     view: doc.defaultView,
957                     ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
958                     keyCode: 0, charCode: 0
959                 },
960                 Mouse: {
961                     type: type,
962                     bubbles: true, cancelable: true,
963                     view: doc.defaultView,
964                     detail: 1,
965                     get screenX() this.view.mozInnerScreenX
966                                 + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
967                     get screenY() this.view.mozInnerScreenY
968                                 + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
969                     clientX: 0,
970                     clientY: 0,
971                     ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
972                     button: 0,
973                     relatedTarget: null
974                 }
975             };
976
977             opts = opts || {};
978             var t = this.constructor.types[type] || "";
979             var evt = doc.createEvent(t + "Events");
980
981             let params = DEFAULTS[t || "HTML"];
982             let args = Object.keys(params);
983             update(params, this.constructor.defaults[type],
984                    iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
985
986             evt["init" + t + "Event"].apply(evt, args.map(k => params[k]));
987             return evt;
988         }
989     }, {
990         init: function init() {
991             // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
992             //       matters, so use that string as the first item, that you
993             //       want to refer to within dactyl's source code for
994             //       comparisons like if (key == "<Esc>") { ... }
995             this.keyTable = {
996                 add: ["+", "Plus", "Add"],
997                 back_quote: ["`"],
998                 back_slash: ["\\"],
999                 back_space: ["BS"],
1000                 comma: [","],
1001                 count: ["count"],
1002                 close_bracket: ["]"],
1003                 delete: ["Del"],
1004                 equals: ["="],
1005                 escape: ["Esc", "Escape"],
1006                 insert: ["Insert", "Ins"],
1007                 leader: ["Leader"],
1008                 left_shift: ["LT", "<"],
1009                 nop: ["Nop"],
1010                 open_bracket: ["["],
1011                 pass: ["Pass"],
1012                 period: ["."],
1013                 quote: ["'"],
1014                 return: ["Return", "CR", "Enter"],
1015                 right_shift: [">"],
1016                 semicolon: [";"],
1017                 slash: ["/"],
1018                 space: ["Space", " "],
1019                 subtract: ["-", "Minus", "Subtract"]
1020             };
1021
1022             this.key_key = {};
1023             this.code_key = {};
1024             this.key_code = {};
1025             this.code_nativeKey = {};
1026
1027             for (let list in values(this.keyTable))
1028                 for (let v in values(list)) {
1029                     if (v.length == 1)
1030                         v = v.toLowerCase();
1031                     this.key_key[v.toLowerCase()] = v;
1032                 }
1033
1034             for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1035                 if (!/^DOM_VK_/.test(k))
1036                     continue;
1037
1038                 this.code_nativeKey[v] = k.substr(4);
1039
1040                 k = k.substr(7).toLowerCase();
1041                 let names = [k.replace(/(^|_)(.)/g, (m, n1, n2) => n2.toUpperCase())
1042                               .replace(/^NUMPAD/, "k")];
1043
1044                 if (names[0].length == 1)
1045                     names[0] = names[0].toLowerCase();
1046
1047                 if (k in this.keyTable)
1048                     names = this.keyTable[k];
1049
1050                 this.code_key[v] = names[0];
1051                 for (let [, name] in Iterator(names)) {
1052                     this.key_key[name.toLowerCase()] = name;
1053                     this.key_code[name.toLowerCase()] = v;
1054                 }
1055             }
1056
1057             // HACK: as Gecko does not include an event for <, we must add this in manually.
1058             if (!("<" in this.key_code)) {
1059                 this.key_code["<"] = 60;
1060                 this.key_code["lt"] = 60;
1061                 this.code_key[60] = "lt";
1062             }
1063
1064             return this;
1065         },
1066
1067         code_key:       Class.Memoize(function (prop) this.init()[prop]),
1068         code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
1069         keyTable:       Class.Memoize(function (prop) this.init()[prop]),
1070         key_code:       Class.Memoize(function (prop) this.init()[prop]),
1071         key_key:        Class.Memoize(function (prop) this.init()[prop]),
1072         pseudoKeys:     RealSet(["count", "leader", "nop", "pass"]),
1073
1074         /**
1075          * Converts a user-input string of keys into a canonical
1076          * representation.
1077          *
1078          * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
1079          * <C- > maps to <C-Space>, <S-a> maps to A
1080          * << maps to <lt><lt>
1081          *
1082          * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1083          * in macros.
1084          *
1085          * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1086          * of x.
1087          *
1088          * @param {string} keys Messy form.
1089          * @param {boolean} unknownOk Whether unknown keys are passed
1090          *     through rather than being converted to <lt>keyname>.
1091          *     @default true
1092          * @returns {string} Canonical form.
1093          */
1094         canonicalKeys: function canonicalKeys(keys, unknownOk=true) {
1095             return this.parse(keys, unknownOk).map(this.bound.stringify).join("");
1096         },
1097
1098         iterKeys: function iterKeys(keys) iter(function () {
1099             let match, re = /<.*?>?>|[^<]/g;
1100             while (match = re.exec(keys))
1101                 yield match[0];
1102         }()),
1103
1104         /**
1105          * Converts an event string into an array of pseudo-event objects.
1106          *
1107          * These objects can be used as arguments to {@link #stringify} or
1108          * {@link DOM.Event}, though they are unlikely to be much use for other
1109          * purposes. They have many of the properties you'd expect to find on a
1110          * real event, but none of the methods.
1111          *
1112          * Also may contain two "special" parameters, .dactylString and
1113          * .dactylShift these are set for characters that can never by
1114          * typed, but may appear in mappings, for example <Nop> is passed as
1115          * dactylString, and dactylShift is set when a user specifies
1116          * <S-@> where @ is a non-case-changeable, non-space character.
1117          *
1118          * @param {string} keys The string to parse.
1119          * @param {boolean} unknownOk Whether unknown keys are passed
1120          *     through rather than being converted to <lt>keyname>.
1121          *     @default true
1122          * @returns {Array[Object]}
1123          */
1124         parse: function parse(input, unknownOk=true) {
1125             if (isArray(input))
1126                 return array.flatten(input.map(k => this.parse(k, unknownOk)));
1127
1128             let out = [];
1129             for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1130                 let evt_str = match[0];
1131
1132                 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1133                                 keyCode: 0, charCode: 0, type: "keypress" };
1134
1135                 if (evt_str.length == 1) {
1136                     evt_obj.charCode = evt_str.charCodeAt(0);
1137                     evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1138                     evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1139                 }
1140                 else {
1141                     let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1142                     modifier = RealSet(modifier.toUpperCase());
1143                     keyname = keyname.toLowerCase();
1144                     evt_obj.dactylKeyname = keyname;
1145                     if (/^u[0-9a-f]+$/.test(keyname))
1146                         keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1147
1148                     if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1149                                     this.key_code[keyname] || this.pseudoKeys.has(keyname))) {
1150                         evt_obj.globKey  = modifier.has("*");
1151                         evt_obj.ctrlKey  = modifier.has("C");
1152                         evt_obj.altKey   = modifier.has("A");
1153                         evt_obj.shiftKey = modifier.has("S");
1154                         evt_obj.metaKey  = modifier.has("M") || modifier.has("⌘");
1155                         evt_obj.dactylShift = evt_obj.shiftKey;
1156
1157                         if (keyname.length == 1) { // normal characters
1158                             if (evt_obj.shiftKey)
1159                                 keyname = keyname.toUpperCase();
1160
1161                             evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1162                             evt_obj.charCode = keyname.charCodeAt(0);
1163                             evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1164                         }
1165                         else if (this.pseudoKeys.has(keyname)) {
1166                             evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1167                         }
1168                         else if (/mouse$/.test(keyname)) { // mouse events
1169                             evt_obj.type = (modifier.has("2") ? "dblclick" : "click");
1170                             evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1171                             delete evt_obj.keyCode;
1172                             delete evt_obj.charCode;
1173                         }
1174                         else { // spaces, control characters, and <
1175                             evt_obj.keyCode = this.key_code[keyname];
1176                             evt_obj.charCode = 0;
1177                         }
1178                     }
1179                     else { // an invalid sequence starting with <, treat as a literal
1180                         out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1181                         continue;
1182                     }
1183                 }
1184
1185                 // TODO: make a list of characters that need keyCode and charCode somewhere
1186                 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1187                     evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1188                 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1189                     evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1190
1191                 evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
1192                                   | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
1193                                   | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1194                                   | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
1195
1196                 out.push(evt_obj);
1197             }
1198             return out;
1199         },
1200
1201         /**
1202          * Converts the specified event to a string in dactyl key-code
1203          * notation. Returns null for an unknown event.
1204          *
1205          * @param {Event} event
1206          * @returns {string}
1207          */
1208         stringify: function stringify(event) {
1209             if (isArray(event))
1210                 return event.map(e => this.stringify(e)).join("");
1211
1212             if (event.dactylString)
1213                 return event.dactylString;
1214
1215             let key = null;
1216             let modifier = "";
1217
1218             if (event.globKey)
1219                 modifier += "*-";
1220             if (event.ctrlKey)
1221                 modifier += "C-";
1222             if (event.altKey)
1223                 modifier += "A-";
1224             if (event.metaKey)
1225                 modifier += "M-";
1226
1227             if (/^key/.test(event.type)) {
1228                 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1229                 if (charCode == 0) {
1230                     if (event.keyCode in this.code_key) {
1231                         key = this.code_key[event.keyCode];
1232
1233                         if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1234                                                || event.ctrlKey || event.altKey || event.metaKey)
1235                                 || event.dactylShift)
1236                             modifier += "S-";
1237                         else if (!modifier && key.length === 1)
1238                             if (event.shiftKey)
1239                                 key = key.toUpperCase();
1240                             else
1241                                 key = key.toLowerCase();
1242
1243                         if (!modifier && key.length == 1)
1244                             return key;
1245                     }
1246                 }
1247                 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1248                 //            (i.e., cntrl codes 27--31)
1249                 // ---
1250                 // For more information, see:
1251                 //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1252                 //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1253                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1254                 //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1255                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1256                 // ---
1257                 //
1258                 // The following fixes are only activated if config.OS.isMacOSX.
1259                 // Technically, they prevent mappings from <C-Esc> (and
1260                 // <C-C-]> if your fancy keyboard permits such things<?>), but
1261                 // these <C-control> mappings are probably pathological (<C-Esc>
1262                 // certainly is on Windows), and so it is probably
1263                 // harmless to remove the config.OS.isMacOSX if desired.
1264                 //
1265                 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1266                     if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1267                         key = "Esc";
1268                         modifier = modifier.replace("C-", "");
1269                     }
1270                     else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1271                         key = String.fromCharCode(charCode + 64);
1272                 }
1273                 // a normal key like a, b, c, 0, etc.
1274                 else if (charCode) {
1275                     key = String.fromCharCode(charCode);
1276
1277                     if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1278                         // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1279                         if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1280                             modifier += "S-";
1281
1282                         key = this.code_key[this.key_code[key]];
1283                     }
1284                     else {
1285                         // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1286                         // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1287                         if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1288                             modifier += "S-";
1289                         if (/^\s$/.test(key))
1290                             key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1291                         else if (modifier.length == 0)
1292                             return key;
1293                     }
1294                 }
1295                 if (key == null) {
1296                     if (event.shiftKey)
1297                         modifier += "S-";
1298                     key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1299                 }
1300                 if (key == null)
1301                     return null;
1302             }
1303             else if (event.type == "click" || event.type == "dblclick") {
1304                 if (event.shiftKey)
1305                     modifier += "S-";
1306                 if (event.type == "dblclick")
1307                     modifier += "2-";
1308                 // TODO: triple and quadruple click
1309
1310                 switch (event.button) {
1311                 case 0:
1312                     key = "LeftMouse";
1313                     break;
1314                 case 1:
1315                     key = "MiddleMouse";
1316                     break;
1317                 case 2:
1318                     key = "RightMouse";
1319                     break;
1320                 }
1321             }
1322
1323             if (key == null)
1324                 return null;
1325
1326             return "<" + modifier + key + ">";
1327         },
1328
1329         defaults: {
1330             load:   { bubbles: false },
1331             submit: { cancelable: true }
1332         },
1333
1334         types: Class.Memoize(() => iter(
1335             {
1336                 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1337                        "hover " +
1338                        "popupshowing popupshown popuphiding popuphidden " +
1339                        "contextmenu",
1340                 Key:   "keydown keypress keyup",
1341                 "":    "change command dactyl-input input submit " +
1342                        "load unload pageshow pagehide DOMContentLoaded " +
1343                        "resize scroll"
1344             }
1345         ).map(([k, v]) => v.split(" ").map(v => [v, k]))
1346          .flatten()
1347          .toObject()),
1348
1349         /**
1350          * Dispatches an event to an element as if it were a native event.
1351          *
1352          * @param {Node} target The DOM node to which to dispatch the event.
1353          * @param {Event} event The event to dispatch.
1354          */
1355         dispatch: function dispatch(target, event, extra) {
1356             try {
1357                 this.feedingEvent = extra;
1358
1359                 if (target instanceof Ci.nsIDOMElement)
1360                     return (target.ownerDocument || target.document || target).defaultView
1361                            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1362                            .dispatchDOMEventViaPresShell(target, event, true);
1363                 else {
1364                     target.dispatchEvent(event);
1365                     return !event.defaultPrevented;
1366                 }
1367             }
1368             catch (e) {
1369                 util.reportError(e);
1370             }
1371             finally {
1372                 this.feedingEvent = null;
1373             }
1374         }
1375     }),
1376
1377     createContents: Class.Memoize(() => services.has("dactyl") && services.dactyl.createContents
1378         || (elem => {})),
1379
1380     isScrollable: Class.Memoize(() => services.has("dactyl") && services.dactyl.getScrollable
1381         ? (elem, dir) => services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1382         : (elem, dir) => true),
1383
1384     isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1385                                     || isObject(val) && "toDOM" in val,
1386
1387     DOMString: function DOMString(val) ({
1388         __proto__: DOMString.prototype,
1389
1390         toDOM: function toDOM(doc) doc.createTextNode(val),
1391
1392         toString: function () val
1393     }),
1394
1395     /**
1396      * The set of input element type attribute values that mark the element as
1397      * an editable field.
1398      */
1399     editableInputs: RealSet(["date", "datetime", "datetime-local", "email", "file",
1400                              "month", "number", "password", "range", "search",
1401                              "tel", "text", "time", "url", "week"]),
1402
1403     /**
1404      * Converts a given DOM Node, Range, or Selection to a string. If
1405      * *html* is true, the output is HTML, otherwise it is presentation
1406      * text.
1407      *
1408      * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1409      *      stringify.
1410      * @param {boolean} html Whether the output should be HTML rather
1411      *      than presentation text.
1412      */
1413     stringify: function stringify(node, html) {
1414         if (node instanceof Ci.nsISelection && node.isCollapsed)
1415             return "";
1416
1417         if (node instanceof Ci.nsIDOMNode) {
1418             let range = node.ownerDocument.createRange();
1419             range.selectNode(node);
1420             node = range;
1421         }
1422         let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1423         doc = doc.ownerDocument || doc;
1424
1425         let encoder = services.HtmlEncoder();
1426         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1427         if (node instanceof Ci.nsISelection)
1428             encoder.setSelection(node);
1429         else if (node instanceof Ci.nsIDOMRange)
1430             encoder.setRange(node);
1431
1432         let str = services.String(encoder.encodeToString());
1433         if (html)
1434             return str.data;
1435
1436         let [result, length] = [{}, {}];
1437         services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1438         return result.value.QueryInterface(Ci.nsISupportsString).data;
1439     },
1440
1441     /**
1442      * Compiles a CSS spec and XPath pattern matcher based on the given
1443      * list. List elements prefixed with "xpath:" are parsed as XPath
1444      * patterns, while other elements are parsed as CSS specs. The
1445      * returned function will, given a node, return an iterator of all
1446      * descendants of that node which match the given specs.
1447      *
1448      * @param {[string]} list The list of patterns to match.
1449      * @returns {function(Node)}
1450      */
1451     compileMatcher: function compileMatcher(list) {
1452         let xpath = [], css = [];
1453         for (let elem in values(list))
1454             if (/^xpath:/.test(elem))
1455                 xpath.push(elem.substr(6));
1456             else
1457                 css.push(elem);
1458
1459         return update(
1460             function matcher(node) {
1461                 if (matcher.xpath)
1462                     for (let elem in DOM.XPath(matcher.xpath, node))
1463                         yield elem;
1464
1465                 if (matcher.css)
1466                     for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1467                         yield elem;
1468             }, {
1469                 css: css.join(", "),
1470                 xpath: xpath.join(" | ")
1471             });
1472     },
1473
1474     /**
1475      * Validates a list as input for {@link #compileMatcher}. Returns
1476      * true if and only if every element of the list is a valid XPath or
1477      * CSS selector.
1478      *
1479      * @param {[string]} list The list of patterns to test
1480      * @returns {boolean} True when the patterns are all valid.
1481      */
1482     validateMatcher: function validateMatcher(list) {
1483         return this.testValues(list, DOM.bound.testMatcher);
1484     },
1485
1486     testMatcher: function testMatcher(value) {
1487         let evaluator = services.XPathEvaluator();
1488         let node = services.XMLDocument();
1489         if (/^xpath:/.test(value))
1490             util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1491         else
1492             util.withProperErrors("querySelector", node, value);
1493         return true;
1494     },
1495
1496     /**
1497      * Converts HTML special characters in *str* to the equivalent HTML
1498      * entities.
1499      *
1500      * @param {string} str
1501      * @param {boolean} simple If true, only escape for the simple case
1502      *     of text nodes.
1503      * @returns {string}
1504      */
1505     escapeHTML: function escapeHTML(str, simple) {
1506         let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
1507         let regexp = simple ? /[<>]/g : /['"&<>]/g;
1508         return str.replace(regexp, m => map[m]);
1509     },
1510
1511     fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1512         if (!doc)
1513             doc = document;
1514
1515         function tag(args, namespaces) {
1516             let _namespaces = namespaces;
1517
1518             // Deal with common error case
1519             if (args == null) {
1520                 util.reportError(Error("Unexpected null when processing XML."));
1521                 args = ["html:i", {}, "[NULL]"];
1522             }
1523
1524             if (isinstance(args, ["String", "Number", "Boolean", _]))
1525                 return doc.createTextNode(args);
1526             if (isObject(args) && "toDOM" in args)
1527                 return args.toDOM(doc, namespaces, nodes);
1528             if (args instanceof Ci.nsIDOMNode)
1529                 return args;
1530             if (args instanceof DOM)
1531                 return args.fragment();
1532             if ("toJSONXML" in args)
1533                 args = args.toJSONXML();
1534
1535             let [name, attr] = args;
1536
1537             if (!isString(name) || args.length == 0 || name === "") {
1538                 var frag = doc.createDocumentFragment();
1539                 Array.forEach(args, function (arg) {
1540                     if (!isArray(arg[0]))
1541                         arg = [arg];
1542                     arg.forEach(function (arg) {
1543                         frag.appendChild(tag(arg, namespaces));
1544                     });
1545                 });
1546                 return frag;
1547             }
1548
1549             attr = attr || {};
1550
1551             function parseNamespace(name) DOM.parseNamespace(name, namespaces);
1552
1553             // FIXME: Surely we can do better.
1554             for (var key in attr) {
1555                 if (/^xmlns(?:$|:)/.test(key)) {
1556                     if (_namespaces === namespaces)
1557                         namespaces = Object.create(namespaces);
1558
1559                     namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1560                 }}
1561
1562             var args = Array.slice(args, 2);
1563             var vals = parseNamespace(name);
1564             var elem = doc.createElementNS(vals[0] || namespaces[""],
1565                                            name);
1566
1567             for (var key in attr)
1568                 if (!/^xmlns(?:$|:)/.test(key)) {
1569                     var val = attr[key];
1570                     if (nodes && key == "key")
1571                         nodes[val] = elem;
1572
1573                     vals = parseNamespace(key);
1574                     if (key == "highlight")
1575                         ;
1576                     else if (typeof val == "function")
1577                         elem.addEventListener(key.replace(/^on/, ""), val, false);
1578                     else
1579                         elem.setAttributeNS(vals[0] || "", key, val);
1580                 }
1581             args.forEach(function (e) {
1582                 elem.appendChild(tag(e, namespaces));
1583             });
1584
1585             if ("highlight" in attr)
1586                 highlight.highlightNode(elem, attr.highlight, nodes || true);
1587             return elem;
1588         }
1589
1590         if (namespaces)
1591             namespaces = update({}, fromJSON.namespaces, namespaces);
1592         else
1593             namespaces = fromJSON.namespaces;
1594
1595         return tag(xml, namespaces);
1596     }, {
1597         namespaces: {
1598             "": "http://www.w3.org/1999/xhtml",
1599             dactyl: String(NS),
1600             html: "http://www.w3.org/1999/xhtml",
1601             xmlns: "http://www.w3.org/2000/xmlns/",
1602             xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
1603         }
1604     }),
1605
1606     toXML: function toXML(xml) {
1607         // Meh. For now.
1608         let doc = services.XMLDocument();
1609         let node = this.fromJSON(xml, doc);
1610         return services.XMLSerializer()
1611                        .serializeToString(node);
1612     },
1613
1614     toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1615         const INDENT = indent || "    ";
1616
1617         const EMPTY = RealSet("area base basefont br col frame hr img input isindex link meta param"
1618                             .split(" "));
1619
1620         function namespaced(namespaces, namespace, localName) {
1621             for (let [k, v] in Iterator(namespaces))
1622                 if (v == namespace)
1623                     return (k ? k + ":" + localName : localName);
1624
1625             throw Error("No such namespace");
1626         }
1627
1628         function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1629
1630         function hasString(args) {
1631             return args.some(a => (isString(a) || isFragment(a) && hasString(a)));
1632         }
1633
1634         function isStrings(args) {
1635             if (!isArray(args))
1636                 return util.dump("ARGS: " + {}.toString.call(args) + " " + args), false;
1637             return args.every(a => (isinstance(a, ["String", DOM.DOMString]) || isFragment(a) && isStrings(a)));
1638         }
1639
1640         function tag(args, namespaces, indent) {
1641             let _namespaces = namespaces;
1642
1643             if (args == "")
1644                 return "";
1645
1646             if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1647                 return indent +
1648                        DOM.escapeHTML(String(args), true);
1649
1650             if (isObject(args) && "toDOM" in args)
1651                 return indent +
1652                        services.XMLSerializer()
1653                                .serializeToString(args.toDOM(services.XMLDocument()))
1654                                .replace(/^/m, indent);
1655
1656             if (args instanceof Ci.nsIDOMNode)
1657                 return indent +
1658                        services.XMLSerializer()
1659                                .serializeToString(args)
1660                                .replace(/^/m, indent);
1661
1662             if ("toJSONXML" in args)
1663                 args = args.toJSONXML();
1664
1665             // Deal with common error case
1666             if (args == null) {
1667                 util.reportError(Error("Unexpected null when processing XML."));
1668                 return "[NULL]";
1669             }
1670
1671             let [name, attr] = args;
1672
1673             if (isFragment(args)) {
1674                 let res = [];
1675                 let join = isArray(args) && isStrings(args) ? "" : "\n";
1676                 Array.forEach(args, function (arg) {
1677                     if (!isArray(arg[0]))
1678                         arg = [arg];
1679
1680                     let contents = [];
1681                     arg.forEach(function (arg) {
1682                         let string = tag(arg, namespaces, indent);
1683                         if (string)
1684                             contents.push(string);
1685                     });
1686                     if (contents.length)
1687                         res.push(contents.join("\n"), join);
1688                 });
1689                 if (res[res.length - 1] == join)
1690                     res.pop();
1691                 return res.join("");
1692             }
1693
1694             attr = attr || {};
1695
1696             function parseNamespace(name) {
1697                 var m = /^(?:(.*):)?(.*)$/.exec(name);
1698                 return [namespaces[m[1]], m[2]];
1699             }
1700
1701             // FIXME: Surely we can do better.
1702             let skipAttr = {};
1703             for (var key in attr) {
1704                 if (/^xmlns(?:$|:)/.test(key)) {
1705                     if (_namespaces === namespaces)
1706                         namespaces = update({}, namespaces);
1707
1708                     let ns = namespaces[attr[key]] || attr[key];
1709                     if (ns == namespaces[key.substr(6)])
1710                         skipAttr[key] = true;
1711
1712                     attr[key] = namespaces[key.substr(6)] = ns;
1713                 }}
1714
1715             var args = Array.slice(args, 2);
1716             var vals = parseNamespace(name);
1717
1718             let res = [indent, "<", name];
1719
1720             for (let [key, val] in Iterator(attr)) {
1721                 if (hasOwnProperty(skipAttr, key))
1722                     continue;
1723
1724                 let vals = parseNamespace(key);
1725                 if (typeof val == "function") {
1726                     key = key.replace(/^(?:on)?/, "on");
1727                     val = val.toSource() + "(event)";
1728                 }
1729
1730                 if (key != "highlight" || vals[0] == String(NS))
1731                     res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1732                 else
1733                     res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1734                              '="', DOM.escapeHTML(val), '"');
1735             }
1736
1737             if ((vals[0] || namespaces[""]) == String(XHTML) && EMPTY.has(vals[1])
1738                     || asXML && !args.length)
1739                 res.push("/>");
1740             else {
1741                 res.push(">");
1742
1743                 if (isStrings(args))
1744                     res.push(args.map(e => tag(e, namespaces, "")).join(""),
1745                              "</", name, ">");
1746                 else {
1747                     let contents = [];
1748                     args.forEach(function (e) {
1749                         let string = tag(e, namespaces, indent + INDENT);
1750                         if (string)
1751                             contents.push(string);
1752                     });
1753
1754                     res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1755                 }
1756             }
1757
1758             return res.join("");
1759         }
1760
1761         if (namespaces)
1762             namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1763         else
1764             namespaces = DOM.fromJSON.namespaces;
1765
1766         return tag(xml, namespaces, "");
1767     },
1768
1769     parseNamespace: function parseNamespace(name, namespaces) {
1770         if (name == "xmlns")
1771             return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1772
1773         var m = /^(?:(.*):)?(.*)$/.exec(name);
1774         return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
1775                 m[2]];
1776     },
1777
1778     /**
1779      * Evaluates an XPath expression in the current or provided
1780      * document. It provides the xhtml, xhtml2 and dactyl XML
1781      * namespaces. The result may be used as an iterator.
1782      *
1783      * @param {string} expression The XPath expression to evaluate.
1784      * @param {Node} elem The context element.
1785      * @param {boolean} asIterator Whether to return the results as an
1786      *     XPath iterator.
1787      * @param {object} namespaces Additional namespaces to recognize.
1788      *     @optional
1789      * @returns {Object} Iterable result of the evaluation.
1790      */
1791     XPath: update(
1792         function XPath(expression, elem, asIterator, namespaces) {
1793             try {
1794                 let doc = elem.ownerDocument || elem;
1795
1796                 if (isArray(expression))
1797                     expression = DOM.makeXPath(expression);
1798
1799                 let resolver = XPath.resolver;
1800                 if (namespaces) {
1801                     namespaces = update({}, DOM.namespaces, namespaces);
1802                     resolver = prefix => namespaces[prefix] || null;
1803                 }
1804
1805                 let result = doc.evaluate(expression, elem,
1806                     resolver,
1807                     asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1808                     null
1809                 );
1810
1811                 let res = {
1812                     iterateNext: function () result.iterateNext(),
1813                     get resultType() result.resultType,
1814                     get snapshotLength() result.snapshotLength,
1815                     snapshotItem: function (i) result.snapshotItem(i),
1816                     __iterator__:
1817                         asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1818                                    : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1819                 };
1820                 return res;
1821             }
1822             catch (e) {
1823                 throw e.stack ? e : Error(e);
1824             }
1825         },
1826         {
1827             resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1828         }),
1829
1830     /**
1831      * Returns an XPath union expression constructed from the specified node
1832      * tests. An expression is built with node tests for both the null and
1833      * XHTML namespaces. See {@link DOM.XPath}.
1834      *
1835      * @param nodes {Array(string)}
1836      * @returns {string}
1837      */
1838     makeXPath: function makeXPath(nodes) {
1839         return array(nodes).map(util.debrace).flatten()
1840                            .map(node => /^[a-z]+:/.test(node) ? node
1841                                                               : [node, "xhtml:" + node])
1842                            .flatten()
1843                            .map(node => "//" + node).join(" | ");
1844     },
1845
1846     namespaces: {
1847         xul: XUL,
1848         xhtml: XHTML,
1849         html: XHTML,
1850         xhtml2: "http://www.w3.org/2002/06/xhtml2",
1851         dactyl: NS
1852     },
1853
1854     namespaceNames: Class.Memoize(function ()
1855         iter(this.namespaces).map(([k, v]) => ([v, k])).toObject()),
1856 });
1857
1858 Object.keys(DOM.Event.types).forEach(function (event) {
1859     let name = event.replace(/-(.)/g, (m, m1) => m1.toUpperCase());
1860     if (!hasOwnProperty(DOM.prototype, name))
1861         DOM.prototype[name] =
1862             function _event(arg, extra) {
1863                 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1864             };
1865 });
1866
1867 var $ = DOM;
1868
1869 endModule();
1870
1871 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1872
1873 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: