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