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("overlay", ["overlay"]);
15 lazyRequire("prefs", ["prefs"]);
16 lazyRequire("template", ["template"]);
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";
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);
29 elem.setAttribute(attr, true);
36 * A jQuery-inspired DOM utility framework.
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.
42 var DOM = Class("DOM", {
43 init: function init(val, context, nodes) {
50 if (context instanceof Ci.nsIDOMDocument)
51 this.document = context;
53 if (typeof val == "string")
54 val = context.querySelectorAll(val);
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);
66 else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
68 else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
70 this[length++] = elem;
71 else if ("length" in val)
72 for (let i = 0; i < val.length; i++)
73 this[length++] = val[i];
81 __iterator__: function __iterator__() {
82 for (let i = 0; i < this.length; i++)
86 Empty: function Empty() this.constructor(null, this.document),
88 nodes: Class.Memoize(function () ({})),
91 for (let i = 0; i < this.length; i++)
95 get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
96 set document(val) this._document = val,
98 attrHooks: array.toObject([
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")
111 matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
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);
120 eachDOM: function eachDOM(val, fn, self) {
122 function munge(val, container, idx) {
123 if (val instanceof Ci.nsIDOMRange)
124 return val.extractContents();
125 if (val instanceof Ci.nsIDOMNode)
128 if (typeof val == "xml" || DOM.isJSONXML(val)) {
129 val = dom.constructor(val, dom.document);
131 container[idx] = val[0];
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));
144 return this.each(function (elem, i) {
145 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
149 util.withProperErrors(fn, self || this, munge(val), this[0], 0);
153 eq: function eq(idx) {
154 return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
157 find: function find(val) {
158 return this.map(function (elem) elem.querySelectorAll(val));
161 findAnon: function findAnon(attr, val) {
162 return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
165 filter: function filter(val, self) {
166 let res = this.Empty();
169 val = this.matcher(val);
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];
180 is: function is(val) {
181 return this.some(this.matcher(val));
184 reverse: function reverse() {
189 all: function all(fn, self) {
190 let res = this.Empty();
192 this.each(function (elem) {
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];
207 map: function map(fn, self) {
208 let res = this.Empty();
209 let obj = self || this.Empty();
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;
223 slice: function eq(start, end) {
224 return this.constructor(Array.slice(this, start, end));
227 some: function some(fn, self) {
228 for (let i = 0; i < this.length; i++)
229 if (fn.call(self || this, this[i], i))
234 get parent() this.map(function (elem) elem.parentNode, this),
236 get offsetParent() this.map(function (elem) {
238 var parent = elem.offsetParent;
239 if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
245 get ancestors() this.all(function (elem) elem.parentNode),
247 get children() this.map(function (elem) Array.filter(elem.childNodes,
248 function (e) e instanceof Ci.nsIDOMElement),
251 get contents() this.map(function (elem) elem.childNodes, this),
253 get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
254 function (e) e != elem && e instanceof Ci.nsIDOMElement),
257 get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
258 get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
260 get allSiblingsBefore() this.all(function (elem) elem.previousSibling),
261 get allSiblingsAfter() this.all(function (elem) elem.nextSibling),
263 get class() let (self = this) ({
264 toString: function () self[0].className,
266 get list() Array.slice(self[0].classList),
267 set list(val) self.attr("class", val.join(" ")),
269 each: function each(meth, arg) {
270 return self.each(function (elem) {
271 elem.classList[meth](arg);
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) {
279 return self.each(function (elem, i) {
280 this.class.toggle(cls, val.call(thisObj || this, elem, i));
282 return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
285 has: function has(cls) this[0].classList.has(cls)
288 get highlight() let (self = this) ({
289 toString: function () self.attrNS(NS, "highlight") || "",
291 get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
293 let str = array.uniq(val).join(" ").trim();
294 self.attrNS(NS, "highlight", str || null);
297 has: function has(hl) ~this.list.indexOf(hl),
299 add: function add(hl) self.each(function () {
300 highlight.loaded[hl] = true;
301 this.highlight.list = this.highlight.list.concat(hl);
304 remove: function remove(hl) self.each(function () {
305 this.highlight.list = this.highlight.list.filter(function (h) h != hl);
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;
312 highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
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() : {},
326 if (node instanceof Ci.nsIDOMDocument)
327 node = node.defaultView;
329 if (node instanceof Ci.nsIDOMWindow)
331 get width() this.right - this.left,
332 get height() this.bottom - this.top,
333 bottom: node.innerHeight,
334 right: node.innerWidth,
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
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 };
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 };
364 let func = callable(left) && left;
366 return this.each(function (elem, i) {
368 ({ left, top }) = func.call(this, elem, i);
370 if (elem instanceof Ci.nsIDOMWindow)
371 elem.scrollTo(left == null ? elem.scrollX : left,
372 top == null ? elem.scrollY : top);
375 elem.scrollLeft = left;
377 elem.scrollTop = top;
383 * Returns true if the given DOM node is currently visible.
387 let style = this[0] && this.style;
388 return style && style.visibility == "visible" && style.display != "none";
395 this[0] instanceof Ci.nsIDOMNSEditableElement;
397 if (this[0].editor instanceof Ci.nsIEditor)
398 var editor = this[0].editor;
406 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
407 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
408 .getEditorForWindow(this[0]);
412 editor instanceof Ci.nsIPlaintextEditor;
413 editor instanceof Ci.nsIHTMLEditor;
417 get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
419 get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
420 Ci.nsIDOMHTMLTextAreaElement,
421 Ci.nsIDOMXULTextBoxElement])
425 * Returns an object representing a Node's computed CSS style.
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;
438 var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
443 util.dumpStack(_("error.nullComputedStyle", node));
444 Cu.reportError(Error(_("error.nullComputedStyle", node)));
451 * Parses the fields of a form and returns a URL/POST-data pair
452 * that is the equivalent of submitting the form.
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.
461 // Nuances gleaned from browser.jar/content/browser/browser.js
463 function encode(name, value, param) {
464 param = param ? "%s" : "";
466 return name + "=" + encodeComponent(value + param);
467 return encodeComponent(name) + "=" + encodeComponent(value) + param;
471 let form = field.form;
472 let doc = form.ownerDocument;
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);
479 converter = services.CharsetConv(cs);
484 let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
485 let url = util.newURI(form.action, charset, uri).spec;
487 let post = form.method.toUpperCase() == "POST";
489 let encodeComponent = encodeURIComponent;
490 if (charset !== "UTF-8")
491 encodeComponent = function encodeComponent(str)
492 escape(converter.ConvertFromUnicode(str) + converter.Finish());
495 if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
496 elems.push(encode(field.name, field.value));
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)) {
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));
510 elems.push(encode(elem.name, "", true));
512 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
513 for (let [, opt] in Iterator(elem.options))
515 elems.push(encode(elem.name, opt.value));
520 return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
521 return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
525 * Generates an XPath expression for the given element.
530 function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
531 if (!(this[0] instanceof Ci.nsIDOMElement))
535 let doc = this.document;
536 for (let elem = this[0];; elem = elem.parentNode) {
537 if (!(elem instanceof Ci.nsIDOMElement))
540 res.push("id(" + quote(elem.id) + ")");
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;
547 name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
549 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
555 return res.reverse().join("/");
559 * Returns a string or XML representation of this node.
561 * @param {boolean} color If true, return a colored, XML
562 * representation of this node.
564 repr: function repr(color) {
565 function namespaced(node) {
566 var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
568 return node.localName;
570 return [["span", { highlight: "HelpXMLNamespace" }, ns],
572 return ns + ":" + node.localName;
576 this.each(function (elem) {
578 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
580 res.push(["span", { highlight: "HelpXML" },
581 ["span", { highlight: "HelpXMLTagStart" },
582 "<", namespaced(elem), " ",
583 template.map(array.iterValues(elem.attributes),
585 ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
586 ["span", { highlight: "HelpXMLString" }, attr.value]
589 !hasChildren ? "/>" : ">",
593 ["span", { highlight: "HtmlTagEnd" },"<", namespaced(elem), ">"]]
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(" ");
600 res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
604 res.push({}.toString.call(elem));
607 res = template.map(res, util.identity, ",");
608 return color ? res : res.join("");
611 attr: function attr(key, val) {
612 return this.attrNS("", key, val);
615 attrNS: function attrNS(ns, key, val) {
616 if (val !== undefined)
617 key = array.toObject([[key, val]]);
619 let hooks = this.attrHooks[ns] || {};
622 return this.each(function (elem, i) {
623 for (let [k, v] in Iterator(key)) {
625 v = v.call(this, elem, i);
627 if (Set.has(hooks, k) && hooks[k].set)
628 hooks[k].set.call(this, elem, v, k);
630 elem.removeAttributeNS(ns, k);
632 elem.setAttributeNS(ns, k, v);
639 if (Set.has(hooks, key) && hooks[key].get)
640 return hooks[key].get.call(this, this[0], key);
642 if (!this[0].hasAttributeNS(ns, key))
645 return this[0].getAttributeNS(ns, key);
648 css: update(function css(key, val) {
649 if (val !== undefined)
650 key = array.toObject([[key, val]]);
653 return this.each(function (elem) {
654 for (let [k, v] in Iterator(key))
655 elem.style[css.property(k)] = v;
658 return this[0].style[css.property(key)];
660 name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
662 property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
665 append: function append(val) {
666 return this.eachDOM(val, function (elem, target) {
667 target.appendChild(elem);
671 prepend: function prepend(val) {
672 return this.eachDOM(val, function (elem, target) {
673 target.insertBefore(elem, target.firstChild);
677 before: function before(val) {
678 return this.eachDOM(val, function (elem, target) {
679 target.parentNode.insertBefore(elem, target);
683 after: function after(val) {
684 return this.eachDOM(val, function (elem, target) {
685 target.parentNode.insertBefore(elem, target.nextSibling);
689 appendTo: function appendTo(elem) {
690 if (!(elem instanceof this.constructor))
691 elem = this.constructor(elem, this.document);
696 prependTo: function prependTo(elem) {
697 if (!(elem instanceof this.constructor))
698 elem = this.constructor(elem, this.document);
703 insertBefore: function insertBefore(elem) {
704 if (!(elem instanceof this.constructor))
705 elem = this.constructor(elem, this.document);
710 insertAfter: function insertAfter(elem) {
711 if (!(elem instanceof this.constructor))
712 elem = this.constructor(elem, this.document);
717 remove: function remove() {
718 return this.each(function (elem) {
720 elem.parentNode.removeChild(elem);
724 empty: function empty() {
725 return this.each(function (elem) {
726 while (elem.firstChild)
727 elem.removeChild(elem.firstChild);
731 fragment: function fragment() {
732 let frag = this.document.createDocumentFragment();
737 clone: function clone(deep)
738 this.map(function (elem) elem.cloneNode(deep)),
740 toggle: function toggle(val, self) {
742 return this.each(function (elem, i) {
743 this[val.call(self || this, elem, i) ? "show" : "hide"]();
746 if (arguments.length)
747 return this[val ? "show" : "hide"]();
749 let hidden = this.map(function (elem) elem.style.display == "none");
750 return this.each(function (elem, i) {
751 this[hidden[i] ? "show" : "hide"]();
754 hide: function hide() {
755 return this.each(function (elem) { elem.style.display = "none"; }, this);
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 = "";
762 this.each(function (elem) {
763 if (!elem.dactylDefaultDisplay)
764 elem.dactylDefaultDisplay = this.style.display;
767 return this.each(function (elem) {
768 elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
772 createContents: function createContents()
773 this.each(DOM.createContents, this),
775 isScrollable: function isScrollable(direction)
776 this.length && DOM.isScrollable(this[0], direction),
778 getSet: function getSet(args, get, set) {
780 return this[0] && get.call(this, this[0]);
782 let [fn, self] = args;
784 fn = function () args[0];
786 return this.each(function (elem, i) {
787 set.call(this, elem, fn.call(self || this, elem, i));
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 }));
797 text: function text(txt, self) {
798 return this.getSet(arguments,
799 function (elem) elem.textContent,
800 function (elem, val) { elem.textContent = val });
803 val: function val(txt) {
804 return this.getSet(arguments,
805 function (elem) elem.value,
806 function (elem, val) { elem.value = val == null ? "" : val });
809 listen: function listen(event, listener, capture) {
813 event = array.toObject([[event, listener]]);
815 for (let [evt, callback] in Iterator(event))
816 event[evt] = util.wrapCallback(callback, true);
818 return this.each(function (elem) {
819 for (let [evt, callback] in Iterator(event))
820 elem.addEventListener(evt, callback, capture);
823 unlisten: function unlisten(event, listener, capture) {
827 event = array.toObject([[event, listener]]);
829 return this.each(function (elem) {
830 for (let [k, v] in Iterator(event))
831 elem.removeEventListener(k, v.wrapper || v, capture);
834 once: function once(event, listener, capture) {
838 event = array.toObject([[event, listener]]);
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);
848 return this.each(function (elem) {
849 for (let [k, v] in Iterator(event))
850 elem.addEventListener(k, v, capture);
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;
863 focus: function focus(arg, extra) {
865 return this.listen("focus", arg, extra);
868 let flags = arg || services.focus.FLAG_BYMOUSE;
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);
886 blur: function blur(arg, extra) {
888 return this.listen("blur", arg, extra);
889 return this.each(function (elem) { elem.blur(); }, this);
893 * Scrolls an element into view if and only if it's not already
896 scrollIntoView: function scrollIntoView(alignWithTop) {
897 return this.each(function (elem) {
898 function getAlignment(viewport) {
899 if (alignWithTop !== undefined)
901 if (rect.bottom < viewport.top)
903 if (rect.top > viewport.bottom)
905 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
909 function fix(parent) {
910 if (!(parent[0] instanceof Ci.nsIDOMWindow)
911 && parent.style.overflow == "visible")
914 ({ rect }) = DOM(elem);
915 let { viewport } = parent;
916 let isect = util.intersection(rect, viewport);
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));
923 parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
928 for (let parent in this.ancestors.items)
931 fix(DOM(this.document.defaultView));
936 * Creates an actual event from a pseudo-event object.
938 * The pseudo-event object (such as may be retrieved from
939 * DOM.Event.parse) should have any properties you want the event to
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
946 Event: Class("Event", {
947 init: function Event(doc, type, opts, target) {
950 type: type, bubbles: true, cancelable: false
954 bubbles: true, cancelable: true,
955 view: doc.defaultView,
956 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
957 keyCode: 0, charCode: 0
961 bubbles: true, cancelable: true,
962 view: doc.defaultView,
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)),
970 ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
977 var t = this.constructor.types[type] || "";
978 var evt = doc.createEvent(t + "Events");
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)));
985 evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
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>") { ... }
995 add: ["+", "Plus", "Add"],
1001 close_bracket: ["]"],
1004 escape: ["Esc", "Escape"],
1005 insert: ["Insert", "Ins"],
1007 left_shift: ["LT", "<"],
1009 open_bracket: ["["],
1013 return: ["Return", "CR", "Enter"],
1017 space: ["Space", " "],
1018 subtract: ["-", "Minus", "Subtract"]
1024 this.code_nativeKey = {};
1026 for (let list in values(this.keyTable))
1027 for (let v in values(list)) {
1029 v = v.toLowerCase();
1030 this.key_key[v.toLowerCase()] = v;
1033 for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
1034 if (!/^DOM_VK_/.test(k))
1037 this.code_nativeKey[v] = k.substr(4);
1039 k = k.substr(7).toLowerCase();
1040 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
1041 .replace(/^NUMPAD/, "k")];
1043 if (names[0].length == 1)
1044 names[0] = names[0].toLowerCase();
1046 if (k in this.keyTable)
1047 names = this.keyTable[k];
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;
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";
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"]),
1075 * Converts a user-input string of keys into a canonical
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>
1082 * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1085 * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1088 * @param {string} keys Messy form.
1089 * @param {boolean} unknownOk Whether unknown keys are passed
1090 * through rather than being converted to <lt>keyname>.
1092 * @returns {string} Canonical form.
1094 canonicalKeys: function canonicalKeys(keys, unknownOk) {
1095 if (arguments.length === 1)
1097 return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1100 iterKeys: function iterKeys(keys) iter(function () {
1101 let match, re = /<.*?>?>|[^<]/g;
1102 while (match = re.exec(keys))
1107 * Converts an event string into an array of pseudo-event objects.
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.
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.
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>.
1124 * @returns {Array[Object]}
1126 parse: function parse(input, unknownOk) {
1128 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1130 if (arguments.length === 1)
1134 for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1135 let evt_str = match[0];
1137 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1138 keyCode: 0, charCode: 0, type: "keypress" };
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();
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));
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;
1162 if (keyname.length == 1) { // normal characters
1163 if (evt_obj.shiftKey)
1164 keyname = keyname.toUpperCase();
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()];
1170 else if (Set.has(this.pseudoKeys, keyname)) {
1171 evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
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;
1179 else { // spaces, control characters, and <
1180 evt_obj.keyCode = this.key_code[keyname];
1181 evt_obj.charCode = 0;
1184 else { // an invalid sequence starting with <, treat as a literal
1185 out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
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>
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);
1207 * Converts the specified event to a string in dactyl key-code
1208 * notation. Returns null for an unknown event.
1210 * @param {Event} event
1213 stringify: function stringify(event) {
1215 return event.map(function (e) this.stringify(e), this).join("");
1217 if (event.dactylString)
1218 return event.dactylString;
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];
1238 if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
1239 || event.ctrlKey || event.altKey || event.metaKey)
1240 || event.dactylShift)
1242 else if (!modifier && key.length === 1)
1244 key = key.toUpperCase();
1246 key = key.toLowerCase();
1248 if (!modifier && key.length == 1)
1252 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1253 // (i.e., cntrl codes 27--31)
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
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.
1270 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1271 if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1273 modifier = modifier.replace("C-", "");
1275 else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1276 key = String.fromCharCode(charCode + 64);
1278 // a normal key like a, b, c, 0, etc.
1279 else if (charCode) {
1280 key = String.fromCharCode(charCode);
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)
1287 key = this.code_key[this.key_code[key]];
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)
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)
1303 key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1308 else if (event.type == "click" || event.type == "dblclick") {
1311 if (event.type == "dblclick")
1313 // TODO: triple and quadruple click
1315 switch (event.button) {
1320 key = "MiddleMouse";
1331 return "<" + modifier + key + ">";
1336 load: { bubbles: false },
1337 submit: { cancelable: true }
1340 types: Class.Memoize(function () iter(
1342 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1344 "popupshowing popupshown popuphiding popuphidden " +
1346 Key: "keydown keypress keyup",
1347 "": "change command dactyl-input input submit " +
1348 "load unload pageshow pagehide DOMContentLoaded " +
1351 ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1356 * Dispatches an event to an element as if it were a native event.
1358 * @param {Node} target The DOM node to which to dispatch the event.
1359 * @param {Event} event The event to dispatch.
1361 dispatch: Class.Memoize(function ()
1362 config.haveGecko("2b")
1363 ? function dispatch(target, event, extra) {
1365 this.feedingEvent = extra;
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);
1373 target.dispatchEvent(event);
1374 return !event.getPreventDefault();
1378 util.reportError(e);
1381 this.feedingEvent = null;
1384 : function dispatch(target, event, extra) {
1386 this.feedingEvent = extra;
1387 target.dispatchEvent(update(event, extra));
1390 this.feedingEvent = null;
1395 createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1396 || function (elem) {}),
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),
1402 isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
1403 || isObject(val) && "toDOM" in val,
1405 DOMString: function DOMString(val) ({
1406 __proto__: DOMString.prototype,
1408 toDOM: function toDOM(doc) doc.createTextNode(val),
1410 toString: function () val
1414 * The set of input element type attribute values that mark the element as
1415 * an editable field.
1417 editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1418 "month", "number", "password", "range", "search",
1419 "tel", "text", "time", "url", "week"]),
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
1426 * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1428 * @param {boolean} html Whether the output should be HTML rather
1429 * than presentation text.
1431 stringify: function stringify(node, html) {
1432 if (node instanceof Ci.nsISelection && node.isCollapsed)
1435 if (node instanceof Ci.nsIDOMNode) {
1436 let range = node.ownerDocument.createRange();
1437 range.selectNode(node);
1440 let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1441 doc = doc.ownerDocument || doc;
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);
1450 let str = services.String(encoder.encodeToString());
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;
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.
1466 * @param {[string]} list The list of patterns to match.
1467 * @returns {function(Node)}
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));
1478 function matcher(node) {
1480 for (let elem in DOM.XPath(matcher.xpath, node))
1484 for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1487 css: css.join(", "),
1488 xpath: xpath.join(" | ")
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
1497 * @param {[string]} list The list of patterns to test
1498 * @returns {boolean} True when the patterns are all valid.
1500 validateMatcher: function validateMatcher(list) {
1501 return this.testValues(list, DOM.closure.testMatcher);
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);
1510 util.withProperErrors("querySelector", node, value);
1515 * Converts HTML special characters in *str* to the equivalent HTML
1518 * @param {string} str
1519 * @param {boolean} simple If true, only escape for the simple case
1523 escapeHTML: function escapeHTML(str, simple) {
1524 let map = { "'": "'", '"': """, "%": "%", "&": "&", "<": "<", ">": ">" };
1525 let regexp = simple ? /[<>]/g : /['"&<>]/g;
1526 return str.replace(regexp, function (m) map[m]);
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.
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.
1540 fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
1541 prefs.get("javascript.options.xml.chrome") !== false
1542 && require("dom-e4x").fromXML }),
1544 fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
1548 function tag(args, namespaces) {
1549 let _namespaces = namespaces;
1551 // Deal with common error case
1553 util.reportError(Error("Unexpected null when processing XML."));
1554 args = ["html:i", {}, "[NULL]"];
1557 if (isinstance(args, ["String", "Number", "Boolean", _]))
1558 return doc.createTextNode(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)
1565 if (args instanceof DOM)
1566 return args.fragment();
1567 if ("toJSONXML" in args)
1568 args = args.toJSONXML();
1570 let [name, attr] = args;
1572 if (!isString(name) || args.length == 0 || name === "") {
1573 var frag = doc.createDocumentFragment();
1574 Array.forEach(args, function (arg) {
1575 if (!isArray(arg[0]))
1577 arg.forEach(function (arg) {
1578 frag.appendChild(tag(arg, namespaces));
1586 function parseNamespace(name) DOM.parseNamespace(name, namespaces);
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);
1594 namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
1597 var args = Array.slice(args, 2);
1598 var vals = parseNamespace(name);
1599 var elem = doc.createElementNS(vals[0] || namespaces[""],
1602 for (var key in attr)
1603 if (!/^xmlns(?:$|:)/.test(key)) {
1604 var val = attr[key];
1605 if (nodes && key == "key")
1608 vals = parseNamespace(key);
1609 if (key == "highlight")
1611 else if (typeof val == "function")
1612 elem.addEventListener(key.replace(/^on/, ""), val, false);
1614 elem.setAttributeNS(vals[0] || "", key, val);
1616 args.forEach(function(e) {
1617 elem.appendChild(tag(e, namespaces));
1620 if ("highlight" in attr)
1621 highlight.highlightNode(elem, attr.highlight, nodes || true);
1626 namespaces = update({}, fromJSON.namespaces, namespaces);
1628 namespaces = fromJSON.namespaces;
1630 return tag(xml, namespaces)
1633 "": "http://www.w3.org/1999/xhtml",
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"
1641 toXML: function toXML(xml) {
1643 let doc = services.XMLDocument();
1644 let node = this.fromJSON(xml, doc);
1645 return services.XMLSerializer()
1646 .serializeToString(node);
1649 toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
1650 const INDENT = indent || " ";
1652 const EMPTY = Set("area base basefont br col frame hr img input isindex link meta param"
1655 function namespaced(namespaces, namespace, localName) {
1656 for (let [k, v] in Iterator(namespaces))
1658 return (k ? k + ":" + localName : localName);
1660 throw Error("No such namespace");
1663 function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
1665 function hasString(args) {
1666 return args.some(function (a) isString(a) || isFragment(a) && hasString(a))
1669 function isStrings(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))
1675 function tag(args, namespaces, indent) {
1676 let _namespaces = namespaces;
1681 if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
1683 DOM.escapeHTML(String(args), true);
1688 .replace(/^/m, indent);
1690 if (isObject(args) && "toDOM" in args)
1692 services.XMLSerializer()
1693 .serializeToString(args.toDOM(services.XMLDocument()))
1694 .replace(/^/m, indent);
1696 if (args instanceof Ci.nsIDOMNode)
1698 services.XMLSerializer()
1699 .serializeToString(args)
1700 .replace(/^/m, indent);
1702 if ("toJSONXML" in args)
1703 args = args.toJSONXML();
1705 // Deal with common error case
1707 util.reportError(Error("Unexpected null when processing XML."));
1711 let [name, attr] = args;
1713 if (isFragment(args)) {
1715 let join = isArray(args) && isStrings(args) ? "" : "\n";
1716 Array.forEach(args, function (arg) {
1717 if (!isArray(arg[0]))
1721 arg.forEach(function (arg) {
1722 let string = tag(arg, namespaces, indent);
1724 contents.push(string);
1726 if (contents.length)
1727 res.push(contents.join("\n"), join)
1729 if (res[res.length - 1] == join)
1731 return res.join("");
1736 function parseNamespace(name) {
1737 var m = /^(?:(.*):)?(.*)$/.exec(name);
1738 return [namespaces[m[1]], m[2]];
1741 // FIXME: Surely we can do better.
1743 for (var key in attr) {
1744 if (/^xmlns(?:$|:)/.test(key)) {
1745 if (_namespaces === namespaces)
1746 namespaces = update({}, namespaces);
1748 let ns = namespaces[attr[key]] || attr[key];
1749 if (ns == namespaces[key.substr(6)])
1750 skipAttr[key] = true;
1752 attr[key] = namespaces[key.substr(6)] = ns;
1755 var args = Array.slice(args, 2);
1756 var vals = parseNamespace(name);
1758 let res = [indent, "<", name];
1760 for (let [key, val] in Iterator(attr)) {
1761 if (Set.has(skipAttr, key))
1764 let vals = parseNamespace(key);
1765 if (typeof val == "function") {
1766 key = key.replace(/^(?:on)?/, "on");
1767 val = val.toSource() + "(event)";
1770 if (key != "highlight" || vals[0] == String(NS))
1771 res.push(" ", key, '="', DOM.escapeHTML(val), '"');
1773 res.push(" ", namespaced(namespaces, String(NS), "highlight"),
1774 '="', DOM.escapeHTML(val), '"');
1777 if ((vals[0] || namespaces[""]) == String(XHTML) && Set.has(EMPTY, vals[1])
1778 || asXML && !args.length)
1783 if (isStrings(args))
1784 res.push(args.map(function (e) tag(e, namespaces, "")).join(""),
1788 args.forEach(function(e) {
1789 let string = tag(e, namespaces, indent + INDENT);
1791 contents.push(string);
1794 res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
1798 return res.join("");
1802 namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
1804 namespaces = DOM.fromJSON.namespaces;
1806 return tag(xml, namespaces, "")
1809 parseNamespace: function parseNamespace(name, namespaces) {
1810 if (name == "xmlns")
1811 return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
1813 var m = /^(?:(.*):)?(.*)$/.exec(name);
1814 return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
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.
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
1827 * @param {object} namespaces Additional namespaces to recognize.
1829 * @returns {Object} Iterable result of the evaluation.
1832 function XPath(expression, elem, asIterator, namespaces) {
1834 let doc = elem.ownerDocument || elem;
1836 if (isArray(expression))
1837 expression = DOM.makeXPath(expression);
1839 let resolver = XPath.resolver;
1841 namespaces = update({}, DOM.namespaces, namespaces);
1842 resolver = function (prefix) namespaces[prefix] || null;
1845 let result = doc.evaluate(expression, elem,
1847 asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1852 iterateNext: function () result.iterateNext(),
1853 get resultType() result.resultType,
1854 get snapshotLength() result.snapshotLength,
1855 snapshotItem: function (i) result.snapshotItem(i),
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); }
1863 throw e.stack ? e : Error(e);
1867 resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
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}.
1875 * @param nodes {Array(string)}
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(" | ");
1888 xhtml2: "http://www.w3.org/2002/06/xhtml2",
1892 namespaceNames: Class.Memoize(function ()
1893 iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
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);
1909 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1911 // vim: set sw=4 ts=4 et ft=javascript: