1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2012 Kris Maglione <maglione.k@gmail.com>
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
9 exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
12 lazyRequire("highlight", ["highlight"]);
13 lazyRequire("messages", ["_"]);
14 lazyRequire("prefs", ["prefs"]);
15 lazyRequire("template", ["template"]);
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";
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);
28 elem.setAttribute(attr, true);
35 * A jQuery-inspired DOM utility framework.
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.
41 var DOM = Class("DOM", {
42 init: function init(val, context, nodes) {
49 if (context instanceof Ci.nsIDOMDocument)
50 this.document = context;
52 if (typeof val == "string")
53 val = context.querySelectorAll(val);
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);
65 else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
67 else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
69 this[length++] = elem;
70 else if ("length" in val)
71 for (let i = 0; i < val.length; i++)
72 this[length++] = val[i];
80 __iterator__: function __iterator__() {
81 for (let i = 0; i < this.length; i++)
85 Empty: function Empty() this.constructor(null, this.document),
87 nodes: Class.Memoize(function () ({})),
90 for (let i = 0; i < this.length; i++)
94 get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
95 set document(val) this._document = val,
97 attrHooks: array.toObject([
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")
110 matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
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);
119 eachDOM: function eachDOM(val, fn, self) {
121 function munge(val, container, idx) {
122 if (val instanceof Ci.nsIDOMRange)
123 return val.extractContents();
124 if (val instanceof Ci.nsIDOMNode)
127 if (typeof val == "xml" || DOM.isJSONXML(val)) {
128 val = dom.constructor(val, dom.document);
130 container[idx] = val[0];
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));
143 return this.each(function (elem, i) {
144 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
148 util.withProperErrors(fn, self || this, munge(val), this[0], 0);
152 eq: function eq(idx) {
153 return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
156 find: function find(val) {
157 return this.map(function (elem) elem.querySelectorAll(val));
160 findAnon: function findAnon(attr, val) {
161 return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
164 filter: function filter(val, self) {
165 let res = this.Empty();
168 val = this.matcher(val);
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];
179 is: function is(val) {
180 return this.some(this.matcher(val));
183 reverse: function reverse() {
188 all: function all(fn, self) {
189 let res = this.Empty();
191 this.each(function (elem) {
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];
206 map: function map(fn, self) {
207 let res = this.Empty();
208 let obj = self || this.Empty();
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;
222 slice: function eq(start, end) {
223 return this.constructor(Array.slice(this, start, end));
226 some: function some(fn, self) {
227 for (let i = 0; i < this.length; i++)
228 if (fn.call(self || this, this[i], i))
233 get parent() this.map(function (elem) elem.parentNode, this),
235 get offsetParent() this.map(function (elem) {
237 var parent = elem.offsetParent;
238 if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
244 get ancestors() this.all(function (elem) elem.parentNode),
246 get children() this.map(function (elem) Array.filter(elem.childNodes,
247 function (e) e instanceof Ci.nsIDOMElement),
250 get contents() this.map(function (elem) elem.childNodes, this),
252 get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
253 function (e) e != elem && e instanceof Ci.nsIDOMElement),
256 get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
257 get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
259 get allSiblingsBefore() this.all(function (elem) elem.previousSibling),
260 get allSiblingsAfter() this.all(function (elem) elem.nextSibling),
262 get class() let (self = this) ({
263 toString: function () self[0].className,
265 get list() Array.slice(self[0].classList),
266 set list(val) self.attr("class", val.join(" ")),
268 each: function each(meth, arg) {
269 return self.each(function (elem) {
270 elem.classList[meth](arg);
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) {
278 return self.each(function (elem, i) {
279 this.class.toggle(cls, val.call(thisObj || this, elem, i));
281 return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
284 has: function has(cls) this[0].classList.has(cls)
287 get highlight() let (self = this) ({
288 toString: function () self.attrNS(NS, "highlight") || "",
290 get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
292 let str = array.uniq(val).join(" ").trim();
293 self.attrNS(NS, "highlight", str || null);
296 has: function has(hl) ~this.list.indexOf(hl),
298 add: function add(hl) self.each(function () {
299 highlight.loaded[hl] = true;
300 this.highlight.list = this.highlight.list.concat(hl);
303 remove: function remove(hl) self.each(function () {
304 this.highlight.list = this.highlight.list.filter(function (h) h != hl);
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;
311 highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
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() : {},
325 if (node instanceof Ci.nsIDOMDocument)
326 node = node.defaultView;
328 if (node instanceof Ci.nsIDOMWindow)
330 get width() this.right - this.left,
331 get height() this.bottom - this.top,
332 bottom: node.innerHeight,
333 right: node.innerWidth,
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
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 };
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 };
363 let func = callable(left) && left;
365 return this.each(function (elem, i) {
367 ({ left, top }) = func.call(this, elem, i);
369 if (elem instanceof Ci.nsIDOMWindow)
370 elem.scrollTo(left == null ? elem.scrollX : left,
371 top == null ? elem.scrollY : top);
374 elem.scrollLeft = left;
376 elem.scrollTop = top;
382 * Returns true if the given DOM node is currently visible.
386 let style = this[0] && this.style;
387 return style && style.visibility == "visible" && style.display != "none";
394 this[0] instanceof Ci.nsIDOMNSEditableElement;
396 if (this[0].editor instanceof Ci.nsIEditor)
397 var editor = this[0].editor;
405 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
406 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
407 .getEditorForWindow(this[0]);
411 editor instanceof Ci.nsIPlaintextEditor;
412 editor instanceof Ci.nsIHTMLEditor;
416 get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
418 get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
419 Ci.nsIDOMHTMLTextAreaElement,
420 Ci.nsIDOMXULTextBoxElement])
424 * Returns an object representing a Node's computed CSS style.
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;
437 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
442 util.dumpStack(_("error.nullComputedStyle", node));
443 Cu.reportError(Error(_("error.nullComputedStyle", node)));
450 * Parses the fields of a form and returns a URL/POST-data pair
451 * that is the equivalent of submitting the form.
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.
460 // Nuances gleaned from browser.jar/content/browser/browser.js
462 function encode(name, value, param) {
463 param = param ? "%s" : "";
465 return name + "=" + encodeComponent(value + param);
466 return encodeComponent(name) + "=" + encodeComponent(value) + param;
470 let form = field.form;
471 let doc = form.ownerDocument;
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);
478 converter = services.CharsetConv(cs);
483 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
484 let url = util.newURI(form.action, charset, uri).spec;
486 let post = form.method.toUpperCase() == "POST";
488 let encodeComponent = encodeURIComponent;
489 if (charset !== "UTF-8")
490 encodeComponent = function encodeComponent(str)
491 escape(converter.ConvertFromUnicode(str) + converter.Finish());
494 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
495 elems.push(encode(field.name, field.value));
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)) {
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));
509 elems.push(encode(elem.name, "", true));
511 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
512 for (let [, opt] in Iterator(elem.options))
514 elems.push(encode(elem.name, opt.value));
519 return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
520 return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
524 * Generates an XPath expression for the given element.
529 function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
530 if (!(this[0] instanceof Ci.nsIDOMElement))
534 let doc = this.document;
535 for (let elem = this[0];; elem = elem.parentNode) {
536 if (!(elem instanceof Ci.nsIDOMElement))
539 res.push("id(" + quote(elem.id) + ")");
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;
546 name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
548 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
554 return res.reverse().join("/");
558 * Returns a string or XML representation of this node.
560 * @param {boolean} color If true, return a colored, XML
561 * representation of this node.
563 repr: function repr(color) {
564 function namespaced(node) {
565 var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
567 return node.localName;
569 return [["span", { highlight: "HelpXMLNamespace" }, ns],
571 return ns + ":" + node.localName;
575 this.each(function (elem) {
577 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
579 res.push(["span", { highlight: "HelpXML" },
580 ["span", { highlight: "HelpXMLTagStart" },
581 "<", namespaced(elem), " ",
582 template.map(array.iterValues(elem.attributes),
584 ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
585 ["span", { highlight: "HelpXMLString" }, attr.value]
588 !hasChildren ? "/>" : ">",
592 ["span", { highlight: "HtmlTagEnd" },"<", namespaced(elem), ">"]]
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(" ");
599 res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
603 res.push({}.toString.call(elem));
606 res = template.map(res, util.identity, ",");
607 return color ? res : res.join("");
610 attr: function attr(key, val) {
611 return this.attrNS("", key, val);
614 attrNS: function attrNS(ns, key, val) {
615 if (val !== undefined)
616 key = array.toObject([[key, val]]);
618 let hooks = this.attrHooks[ns] || {};
621 return this.each(function (elem, i) {
622 for (let [k, v] in Iterator(key)) {
624 v = v.call(this, elem, i);
626 if (Set.has(hooks, k) && hooks[k].set)
627 hooks[k].set.call(this, elem, v, k);
629 elem.removeAttributeNS(ns, k);
631 elem.setAttributeNS(ns, k, v);
638 if (Set.has(hooks, key) && hooks[key].get)
639 return hooks[key].get.call(this, this[0], key);
641 if (!this[0].hasAttributeNS(ns, key))
644 return this[0].getAttributeNS(ns, key);
647 css: update(function css(key, val) {
648 if (val !== undefined)
649 key = array.toObject([[key, val]]);
652 return this.each(function (elem) {
653 for (let [k, v] in Iterator(key))
654 elem.style[css.property(k)] = v;
657 return this[0].style[css.property(key)];
659 name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
661 property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
664 append: function append(val) {
665 return this.eachDOM(val, function (elem, target) {
666 target.appendChild(elem);
670 prepend: function prepend(val) {
671 return this.eachDOM(val, function (elem, target) {
672 target.insertBefore(elem, target.firstChild);
676 before: function before(val) {
677 return this.eachDOM(val, function (elem, target) {
678 target.parentNode.insertBefore(elem, target);
682 after: function after(val) {
683 return this.eachDOM(val, function (elem, target) {
684 target.parentNode.insertBefore(elem, target.nextSibling);
688 appendTo: function appendTo(elem) {
689 if (!(elem instanceof this.constructor))
690 elem = this.constructor(elem, this.document);
695 prependTo: function prependTo(elem) {
696 if (!(elem instanceof this.constructor))
697 elem = this.constructor(elem, this.document);
702 insertBefore: function insertBefore(elem) {
703 if (!(elem instanceof this.constructor))
704 elem = this.constructor(elem, this.document);
709 insertAfter: function insertAfter(elem) {
710 if (!(elem instanceof this.constructor))
711 elem = this.constructor(elem, this.document);
716 remove: function remove() {
717 return this.each(function (elem) {
719 elem.parentNode.removeChild(elem);
723 empty: function empty() {
724 return this.each(function (elem) {
725 while (elem.firstChild)
726 elem.removeChild(elem.firstChild);
730 fragment: function fragment() {
731 let frag = this.document.createDocumentFragment();
736 clone: function clone(deep)
737 this.map(function (elem) elem.cloneNode(deep)),
739 toggle: function toggle(val, self) {
741 return this.each(function (elem, i) {
742 this[val.call(self || this, elem, i) ? "show" : "hide"]();
745 if (arguments.length)
746 return this[val ? "show" : "hide"]();
748 let hidden = this.map(function (elem) elem.style.display == "none");
749 return this.each(function (elem, i) {
750 this[hidden[i] ? "show" : "hide"]();
753 hide: function hide() {
754 return this.each(function (elem) { elem.style.display = "none"; }, this);
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 = "";
761 this.each(function (elem) {
762 if (!elem.dactylDefaultDisplay)
763 elem.dactylDefaultDisplay = this.style.display;
766 return this.each(function (elem) {
767 elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
771 createContents: function createContents()
772 this.each(DOM.createContents, this),
774 isScrollable: function isScrollable(direction)
775 this.length && DOM.isScrollable(this[0], direction),
777 getSet: function getSet(args, get, set) {
779 return this[0] && get.call(this, this[0]);
781 let [fn, self] = args;
783 fn = function () args[0];
785 return this.each(function (elem, i) {
786 set.call(this, elem, fn.call(self || this, elem, i));
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 }));
796 text: function text(txt, self) {
797 return this.getSet(arguments,
798 function (elem) elem.textContent,
799 function (elem, val) { elem.textContent = val });
802 val: function val(txt) {
803 return this.getSet(arguments,
804 function (elem) elem.value,
805 function (elem, val) { elem.value = val == null ? "" : val });
808 listen: function listen(event, listener, capture) {
812 event = array.toObject([[event, listener]]);
814 for (let [evt, callback] in Iterator(event))
815 event[evt] = util.wrapCallback(callback, true);
817 return this.each(function (elem) {
818 for (let [evt, callback] in Iterator(event))
819 elem.addEventListener(evt, callback, capture);
822 unlisten: function unlisten(event, listener, capture) {
826 event = array.toObject([[event, listener]]);
828 return this.each(function (elem) {
829 for (let [k, v] in Iterator(event))
830 elem.removeEventListener(k, v.wrapper || v, capture);
833 once: function once(event, listener, capture) {
837 event = array.toObject([[event, listener]]);
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);
847 return this.each(function (elem) {
848 for (let [k, v] in Iterator(event))
849 elem.addEventListener(k, v, capture);
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;
862 focus: function focus(arg, extra) {
864 return this.listen("focus", arg, extra);
867 let flags = arg || services.focus.FLAG_BYMOUSE;
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);
885 blur: function blur(arg, extra) {
887 return this.listen("blur", arg, extra);
888 return this.each(function (elem) { elem.blur(); }, this);
892 * Scrolls an element into view if and only if it's not already
895 scrollIntoView: function scrollIntoView(alignWithTop) {
896 return this.each(function (elem) {
897 function getAlignment(viewport) {
898 if (alignWithTop !== undefined)
900 if (rect.bottom < viewport.top)
902 if (rect.top > viewport.bottom)
904 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
908 function fix(parent) {
909 if (!(parent[0] instanceof Ci.nsIDOMWindow)
910 && parent.style.overflow == "visible")
913 ({ rect }) = DOM(elem);
914 let { viewport } = parent;
915 let isect = util.intersection(rect, viewport);
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));
922 parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
927 for (let parent in this.ancestors.items)
930 fix(DOM(this.document.defaultView));
935 * Creates an actual event from a pseudo-event object.
937 * The pseudo-event object (such as may be retrieved from
938 * DOM.Event.parse) should have any properties you want the event to
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
945 Event: Class("Event", {
946 init: function Event(doc, type, opts, target) {
949 type: type, bubbles: true, cancelable: false
953 bubbles: true, cancelable: true,
954 view: doc.defaultView,
955 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
956 keyCode: 0, charCode: 0
960 bubbles: true, cancelable: true,
961 view: doc.defaultView,
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)),
969 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
976 var t = this.constructor.types[type] || "";
977 var evt = doc.createEvent(t + "Events");
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)));
984 evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
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>") { ... }
994 add: ["+", "Plus", "Add"],
1000 close_bracket: ["]"],
1003 escape: ["Esc", "Escape"],
1004 insert: ["Insert", "Ins"],
1006 left_shift: ["LT", "<"],
1008 open_bracket: ["["],
1012 return: ["Return", "CR", "Enter"],
1016 space: ["Space", " "],
1017 subtract: ["-", "Minus", "Subtract"]
1023 this.code_nativeKey = {};
1025 for (let list in values(this.keyTable))
1026 for (let v in values(list)) {
1028 v = v.toLowerCase();
1029 this.key_key[v.toLowerCase()] = v;
1032 for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1033 if (!/^DOM_VK_/.test(k))
1036 this.code_nativeKey[v] = k.substr(4);
1038 k = k.substr(7).toLowerCase();
1039 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
1040 .replace(/^NUMPAD/, "k")];
1042 if (names[0].length == 1)
1043 names[0] = names[0].toLowerCase();
1045 if (k in this.keyTable)
1046 names = this.keyTable[k];
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;
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";
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"]),
1074 * Converts a user-input string of keys into a canonical
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>
1081 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1084 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1087 * @param {string} keys Messy form.
1088 * @param {boolean} unknownOk Whether unknown keys are passed
1089 * through rather than being converted to <lt>keyname>.
1091 * @returns {string} Canonical form.
1093 canonicalKeys: function canonicalKeys(keys, unknownOk) {
1094 if (arguments.length === 1)
1096 return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1099 iterKeys: function iterKeys(keys) iter(function () {
1100 let match, re = /<.*?>?>|[^<]/g;
1101 while (match = re.exec(keys))
1106 * Converts an event string into an array of pseudo-event objects.
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.
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.
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>.
1123 * @returns {Array[Object]}
1125 parse: function parse(input, unknownOk) {
1127 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1129 if (arguments.length === 1)
1133 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1134 let evt_str = match[0];
1136 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1137 keyCode: 0, charCode: 0, type: "keypress" };
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();
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));
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;
1161 if (keyname.length == 1) { // normal characters
1162 if (evt_obj.shiftKey)
1163 keyname = keyname.toUpperCase();
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()];
1169 else if (Set.has(this.pseudoKeys, keyname)) {
1170 evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
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;
1178 else { // spaces, control characters, and <
1179 evt_obj.keyCode = this.key_code[keyname];
1180 evt_obj.charCode = 0;
1183 else { // an invalid sequence starting with <, treat as a literal
1184 out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
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>
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);
1206 * Converts the specified event to a string in dactyl key-code
1207 * notation. Returns null for an unknown event.
1209 * @param {Event} event
1212 stringify: function stringify(event) {
1214 return event.map(function (e) this.stringify(e), this).join("");
1216 if (event.dactylString)
1217 return event.dactylString;
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];
1237 if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1238 || event.ctrlKey || event.altKey || event.metaKey)
1239 || event.dactylShift)
1241 else if (!modifier && key.length === 1)
1243 key = key.toUpperCase();
1245 key = key.toLowerCase();
1247 if (!modifier && key.length == 1)
1251 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1252 // (i.e., cntrl codes 27--31)
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
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.
1269 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1270 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1272 modifier = modifier.replace("C-", "");
1274 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1275 key = String.fromCharCode(charCode + 64);
1277 // a normal key like a, b, c, 0, etc.
1278 else if (charCode) {
1279 key = String.fromCharCode(charCode);
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)
1286 key = this.code_key[this.key_code[key]];
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)
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)
1302 key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1307 else if (event.type == "click" || event.type == "dblclick") {
1310 if (event.type == "dblclick")
1312 // TODO: triple and quadruple click
1314 switch (event.button) {
1319 key = "MiddleMouse";
1330 return "<" + modifier + key + ">";
1335 load: { bubbles: false },
1336 submit: { cancelable: true }
1339 types: Class.Memoize(function () iter(
1341 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1343 "popupshowing popupshown popuphiding popuphidden " +
1345 Key: "keydown keypress keyup",
1346 "": "change command dactyl-input input submit " +
1347 "load unload pageshow pagehide DOMContentLoaded " +
1350 ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1355 * Dispatches an event to an element as if it were a native event.
1357 * @param {Node} target The DOM node to which to dispatch the event.
1358 * @param {Event} event The event to dispatch.
1360 dispatch: Class.Memoize(function ()
1361 config.haveGecko("2b")
1362 ? function dispatch(target, event, extra) {
1364 this.feedingEvent = extra;
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);
1372 target.dispatchEvent(event);
1373 return !event.getPreventDefault();
1377 util.reportError(e);
1380 this.feedingEvent = null;
1383 : function dispatch(target, event, extra) {
1385 this.feedingEvent = extra;
1386 target.dispatchEvent(update(event, extra));
1389 this.feedingEvent = null;
1394 createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1395 || function (elem) {}),
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),
1401 isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1402 || isObject(val) && "toDOM" in val,
1404 DOMString: function DOMString(val) ({
1405 __proto__: DOMString.prototype,
1407 toDOM: function toDOM(doc) doc.createTextNode(val),
1409 toString: function () val
1413 * The set of input element type attribute values that mark the element as
1414 * an editable field.
1416 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1417 "month", "number", "password", "range", "search",
1418 "tel", "text", "time", "url", "week"]),
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
1425 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1427 * @param {boolean} html Whether the output should be HTML rather
1428 * than presentation text.
1430 stringify: function stringify(node, html) {
1431 if (node instanceof Ci.nsISelection && node.isCollapsed)
1434 if (node instanceof Ci.nsIDOMNode) {
1435 let range = node.ownerDocument.createRange();
1436 range.selectNode(node);
1439 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1440 doc = doc.ownerDocument || doc;
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);
1449 let str = services.String(encoder.encodeToString());
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;
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.
1465 * @param {[string]} list The list of patterns to match.
1466 * @returns {function(Node)}
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));
1477 function matcher(node) {
1479 for (let elem in DOM.XPath(matcher.xpath, node))
1483 for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1486 css: css.join(", "),
1487 xpath: xpath.join(" | ")
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
1496 * @param {[string]} list The list of patterns to test
1497 * @returns {boolean} True when the patterns are all valid.
1499 validateMatcher: function validateMatcher(list) {
1500 return this.testValues(list, DOM.closure.testMatcher);
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);
1509 util.withProperErrors("querySelector", node, value);
1514 * Converts HTML special characters in *str* to the equivalent HTML
1517 * @param {string} str
1518 * @param {boolean} simple If true, only escape for the simple case
1522 escapeHTML: function escapeHTML(str, simple) {
1523 let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
1524 let regexp = simple ? /[<>]/g : /['"&<>]/g;
1525 return str.replace(regexp, function (m) map[m]);
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.
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.
1539 fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
1540 prefs.get("javascript.options.xml.chrome") !== false
1541 && require("dom-e4x").fromXML }),
1543 fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1547 function tag(args, namespaces) {
1548 let _namespaces = namespaces;
1550 // Deal with common error case
1552 util.reportError(Error("Unexpected null when processing XML."));
1553 args = ["html:i", {}, "[NULL]"];
1556 if (isinstance(args, ["String", "Number", "Boolean", _]))
1557 return doc.createTextNode(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)
1564 if (args instanceof DOM)
1565 return args.fragment();
1566 if ("toJSONXML" in args)
1567 args = args.toJSONXML();
1569 let [name, attr] = args;
1571 if (!isString(name) || args.length == 0 || name === "") {
1572 var frag = doc.createDocumentFragment();
1573 Array.forEach(args, function (arg) {
1574 if (!isArray(arg[0]))
1576 arg.forEach(function (arg) {
1577 frag.appendChild(tag(arg, namespaces));
1585 function parseNamespace(name) DOM.parseNamespace(name, namespaces);
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);
1593 namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1596 var args = Array.slice(args, 2);
1597 var vals = parseNamespace(name);
1598 var elem = doc.createElementNS(vals[0] || namespaces[""],
1601 for (var key in attr)
1602 if (!/^xmlns(?:$|:)/.test(key)) {
1603 var val = attr[key];
1604 if (nodes && key == "key")
1607 vals = parseNamespace(key);
1608 if (key == "highlight")
1610 else if (typeof val == "function")
1611 elem.addEventListener(key.replace(/^on/, ""), val, false);
1613 elem.setAttributeNS(vals[0] || "", key, val);
1615 args.forEach(function(e) {
1616 elem.appendChild(tag(e, namespaces));
1619 if ("highlight" in attr)
1620 highlight.highlightNode(elem, attr.highlight, nodes || true);
1625 namespaces = update({}, fromJSON.namespaces, namespaces);
1627 namespaces = fromJSON.namespaces;
1629 return tag(xml, namespaces)
1632 "": "http://www.w3.org/1999/xhtml",
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"
1640 toXML: function toXML(xml) {
1642 let doc = services.XMLDocument();
1643 let node = this.fromJSON(xml, doc);
1644 return services.XMLSerializer()
1645 .serializeToString(node);
1648 toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1649 const INDENT = indent || " ";
1651 const EMPTY = Set("area base basefont br col frame hr img input isindex link meta param"
1654 function namespaced(namespaces, namespace, localName) {
1655 for (let [k, v] in Iterator(namespaces))
1657 return (k ? k + ":" + localName : localName);
1659 throw Error("No such namespace");
1662 function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1664 function hasString(args) {
1665 return args.some(function (a) isString(a) || isFragment(a) && hasString(a))
1668 function isStrings(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))
1674 function tag(args, namespaces, indent) {
1675 let _namespaces = namespaces;
1680 if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1682 DOM.escapeHTML(String(args), true);
1687 .replace(/^/m, indent);
1689 if (isObject(args) && "toDOM" in args)
1691 services.XMLSerializer()
1692 .serializeToString(args.toDOM(services.XMLDocument()))
1693 .replace(/^/m, indent);
1695 if (args instanceof Ci.nsIDOMNode)
1697 services.XMLSerializer()
1698 .serializeToString(args)
1699 .replace(/^/m, indent);
1701 if ("toJSONXML" in args)
1702 args = args.toJSONXML();
1704 // Deal with common error case
1706 util.reportError(Error("Unexpected null when processing XML."));
1710 let [name, attr] = args;
1712 if (isFragment(args)) {
1714 let join = isArray(args) && isStrings(args) ? "" : "\n";
1715 Array.forEach(args, function (arg) {
1716 if (!isArray(arg[0]))
1720 arg.forEach(function (arg) {
1721 let string = tag(arg, namespaces, indent);
1723 contents.push(string);
1725 if (contents.length)
1726 res.push(contents.join("\n"), join)
1728 if (res[res.length - 1] == join)
1730 return res.join("");
1735 function parseNamespace(name) {
1736 var m = /^(?:(.*):)?(.*)$/.exec(name);
1737 return [namespaces[m[1]], m[2]];
1740 // FIXME: Surely we can do better.
1742 for (var key in attr) {
1743 if (/^xmlns(?:$|:)/.test(key)) {
1744 if (_namespaces === namespaces)
1745 namespaces = update({}, namespaces);
1747 let ns = namespaces[attr[key]] || attr[key];
1748 if (ns == namespaces[key.substr(6)])
1749 skipAttr[key] = true;
1751 attr[key] = namespaces[key.substr(6)] = ns;
1754 var args = Array.slice(args, 2);
1755 var vals = parseNamespace(name);
1757 let res = [indent, "<", name];
1759 for (let [key, val] in Iterator(attr)) {
1760 if (Set.has(skipAttr, key))
1763 let vals = parseNamespace(key);
1764 if (typeof val == "function") {
1765 key = key.replace(/^(?:on)?/, "on");
1766 val = val.toSource() + "(event)";
1769 if (key != "highlight" || vals[0] == String(NS))
1770 res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1772 res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1773 '="', DOM.escapeHTML(val), '"');
1776 if ((vals[0] || namespaces[""]) == String(XHTML) && Set.has(EMPTY, vals[1])
1777 || asXML && !args.length)
1782 if (isStrings(args))
1783 res.push(args.map(function (e) tag(e, namespaces, "")).join(""),
1787 args.forEach(function(e) {
1788 let string = tag(e, namespaces, indent + INDENT);
1790 contents.push(string);
1793 res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1797 return res.join("");
1801 namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1803 namespaces = DOM.fromJSON.namespaces;
1805 return tag(xml, namespaces, "")
1808 parseNamespace: function parseNamespace(name, namespaces) {
1809 if (name == "xmlns")
1810 return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1812 var m = /^(?:(.*):)?(.*)$/.exec(name);
1813 return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
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.
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
1826 * @param {object} namespaces Additional namespaces to recognize.
1828 * @returns {Object} Iterable result of the evaluation.
1831 function XPath(expression, elem, asIterator, namespaces) {
1833 let doc = elem.ownerDocument || elem;
1835 if (isArray(expression))
1836 expression = DOM.makeXPath(expression);
1838 let resolver = XPath.resolver;
1840 namespaces = update({}, DOM.namespaces, namespaces);
1841 resolver = function (prefix) namespaces[prefix] || null;
1844 let result = doc.evaluate(expression, elem,
1846 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1851 iterateNext: function () result.iterateNext(),
1852 get resultType() result.resultType,
1853 get snapshotLength() result.snapshotLength,
1854 snapshotItem: function (i) result.snapshotItem(i),
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); }
1862 throw e.stack ? e : Error(e);
1866 resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
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}.
1874 * @param nodes {Array(string)}
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(" | ");
1887 xhtml2: "http://www.w3.org/2002/06/xhtml2",
1891 namespaceNames: Class.Memoize(function ()
1892 iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
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);
1908 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1910 // vim: set sw=4 ts=4 et ft=javascript: