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