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