<?xml version="1.0"?> <!-- This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> <bindings id="richlistboxBindings" xmlns="http://www.mozilla.org/xbl" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xbl="http://www.mozilla.org/xbl"> <binding id="richlistbox" extends="chrome://global/content/bindings/listbox.xml#listbox-base"> <resources> <stylesheet src="chrome://global/skin/richlistbox.css"/> </resources> <content> <children includes="listheader"/> <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box" flex="1" style="overflow: auto;" xbl:inherits="dir,pack"> <children/> </xul:scrollbox> </content> <implementation> <field name="_scrollbox"> document.getAnonymousElementByAttribute(this, "anonid", "main-box"); </field> <field name="scrollBoxObject"> this._scrollbox.boxObject; </field> <constructor> <![CDATA[ // add a template build listener if (this.builder) this.builder.addListener(this._builderListener); else this._refreshSelection(); ]]> </constructor> <destructor> <![CDATA[ // remove the template build listener if (this.builder) this.builder.removeListener(this._builderListener); ]]> </destructor> <!-- Overriding baselistbox --> <method name="_fireOnSelect"> <body> <![CDATA[ // make sure not to modify last-selected when suppressing select events // (otherwise we'll lose the selection when a template gets rebuilt) if (this._suppressOnSelect || this.suppressOnSelect) return; // remember the current item and all selected items with IDs var state = this.currentItem ? this.currentItem.id : ""; if (this.selType == "multiple" && this.selectedCount) { let getId = function getId(aItem) { return aItem.id; } state += " " + [... this.selectedItems].filter(getId).map(getId).join(" "); } if (state) this.setAttribute("last-selected", state); else this.removeAttribute("last-selected"); // preserve the index just in case no IDs are available if (this.currentIndex > -1) this._currentIndex = this.currentIndex + 1; var event = document.createEvent("Events"); event.initEvent("select", true, true); this.dispatchEvent(event); // always call this (allows a commandupdater without controller) document.commandDispatcher.updateCommands("richlistbox-select"); ]]> </body> </method> <!-- We override base-listbox here because those methods don't take dir into account on listbox (which doesn't support dir yet) --> <method name="getNextItem"> <parameter name="aStartItem"/> <parameter name="aDelta"/> <body> <![CDATA[ var prop = this.dir == "reverse" && this._mayReverse ? "previousSibling" : "nextSibling"; while (aStartItem) { aStartItem = aStartItem[prop]; if (aStartItem && aStartItem instanceof Components.interfaces.nsIDOMXULSelectControlItemElement && (!this._userSelecting || this._canUserSelect(aStartItem))) { --aDelta; if (aDelta == 0) return aStartItem; } } return null; ]]></body> </method> <method name="getPreviousItem"> <parameter name="aStartItem"/> <parameter name="aDelta"/> <body> <![CDATA[ var prop = this.dir == "reverse" && this._mayReverse ? "nextSibling" : "previousSibling"; while (aStartItem) { aStartItem = aStartItem[prop]; if (aStartItem && aStartItem instanceof Components.interfaces.nsIDOMXULSelectControlItemElement && (!this._userSelecting || this._canUserSelect(aStartItem))) { --aDelta; if (aDelta == 0) return aStartItem; } } return null; ]]> </body> </method> <method name="appendItem"> <parameter name="aLabel"/> <parameter name="aValue"/> <body> return this.insertItemAt(-1, aLabel, aValue); </body> </method> <method name="insertItemAt"> <parameter name="aIndex"/> <parameter name="aLabel"/> <parameter name="aValue"/> <body> const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; var item = this.ownerDocument.createElementNS(XULNS, "richlistitem"); item.setAttribute("value", aValue); var label = this.ownerDocument.createElementNS(XULNS, "label"); label.setAttribute("value", aLabel); label.setAttribute("flex", "1"); label.setAttribute("crop", "end"); item.appendChild(label); var before = this.getItemAtIndex(aIndex); if (!before) this.appendChild(item); else this.insertBefore(item, before); return item; </body> </method> <property name="itemCount" readonly="true" onget="return this.children.length"/> <method name="getIndexOfItem"> <parameter name="aItem"/> <body> <![CDATA[ // don't search the children, if we're looking for none of them if (aItem == null) return -1; return this.children.indexOf(aItem); ]]> </body> </method> <method name="getItemAtIndex"> <parameter name="aIndex"/> <body> return this.children[aIndex] || null; </body> </method> <method name="ensureIndexIsVisible"> <parameter name="aIndex"/> <body> <![CDATA[ // work around missing implementation in scrollBoxObject return this.ensureElementIsVisible(this.getItemAtIndex(aIndex)); ]]> </body> </method> <method name="ensureElementIsVisible"> <parameter name="aElement"/> <body> <![CDATA[ if (!aElement) return; var targetRect = aElement.getBoundingClientRect(); var scrollRect = this._scrollbox.getBoundingClientRect(); var offset = targetRect.top - scrollRect.top; if (offset >= 0) { // scrollRect.bottom wouldn't take a horizontal scroll bar into account let scrollRectBottom = scrollRect.top + this._scrollbox.clientHeight; offset = targetRect.bottom - scrollRectBottom; if (offset <= 0) return; } this._scrollbox.scrollTop += offset; ]]> </body> </method> <method name="scrollToIndex"> <parameter name="aIndex"/> <body> <![CDATA[ var item = this.getItemAtIndex(aIndex); if (item) this.scrollBoxObject.scrollToElement(item); ]]> </body> </method> <method name="getNumberOfVisibleRows"> <!-- returns the number of currently visible rows --> <!-- don't rely on this function, if the items' height can vary! --> <body> <![CDATA[ var children = this.children; for (var top = 0; top < children.length && !this._isItemVisible(children[top]); top++); for (var ix = top; ix < children.length && this._isItemVisible(children[ix]); ix++); return ix - top; ]]> </body> </method> <method name="getIndexOfFirstVisibleRow"> <body> <![CDATA[ var children = this.children; for (var ix = 0; ix < children.length; ix++) if (this._isItemVisible(children[ix])) return ix; return -1; ]]> </body> </method> <method name="getRowCount"> <body> <![CDATA[ return this.children.length; ]]> </body> </method> <method name="scrollOnePage"> <parameter name="aDirection"/> <!-- Must be -1 or 1 --> <body> <![CDATA[ var children = this.children; if (children.length == 0) return 0; // If nothing is selected, we just select the first element // at the extreme we're moving away from if (!this.currentItem) return aDirection == -1 ? children.length : 0; // If the current item is visible, scroll by one page so that // the new current item is at approximately the same position as // the existing current item. if (this._isItemVisible(this.currentItem)) this.scrollBoxObject.scrollBy(0, this.scrollBoxObject.height * aDirection); // Figure out, how many items fully fit into the view port // (including the currently selected one), and determine // the index of the first one lying (partially) outside var height = this.scrollBoxObject.height; var startBorder = this.currentItem.boxObject.y; if (aDirection == -1) startBorder += this.currentItem.boxObject.height; var index = this.currentIndex; for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) { var boxObject = children[ix].boxObject; if (boxObject.height == 0) continue; // hidden children have a y of 0 var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0); if ((endBorder - startBorder) * aDirection > height) break; // we've reached the desired distance index = ix; } return index != this.currentIndex ? index - this.currentIndex : aDirection; ]]> </body> </method> <!-- richlistbox specific --> <property name="children" readonly="true"> <getter> <![CDATA[ var childNodes = []; var isReverse = this.dir == "reverse" && this._mayReverse; var child = isReverse ? this.lastChild : this.firstChild; var prop = isReverse ? "previousSibling" : "nextSibling"; while (child) { if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement) childNodes.push(child); child = child[prop]; } return childNodes; ]]> </getter> </property> <field name="_builderListener" readonly="true"> <![CDATA[ ({ mOuter: this, item: null, willRebuild: function(builder) { }, didRebuild: function(builder) { this.mOuter._refreshSelection(); } }); ]]> </field> <method name="_refreshSelection"> <body> <![CDATA[ // when this method is called, we know that either the currentItem // and selectedItems we have are null (ctor) or a reference to an // element no longer in the DOM (template). // first look for the last-selected attribute var state = this.getAttribute("last-selected"); if (state) { var ids = state.split(" "); var suppressSelect = this._suppressOnSelect; this._suppressOnSelect = true; this.clearSelection(); for (let i = 1; i < ids.length; i++) { var selectedItem = document.getElementById(ids[i]); if (selectedItem) this.addItemToSelection(selectedItem); } var currentItem = document.getElementById(ids[0]); if (!currentItem && this._currentIndex) currentItem = this.getItemAtIndex(Math.min( this._currentIndex - 1, this.getRowCount())); if (currentItem) { this.currentItem = currentItem; if (this.selType != "multiple" && this.selectedCount == 0) this.selectedItem = currentItem; if (this.scrollBoxObject.height) { this.ensureElementIsVisible(currentItem); } else { // XXX hack around a bug in ensureElementIsVisible as it will // scroll beyond the last element, bug 493645. var previousElement = this.dir == "reverse" ? currentItem.nextSibling : currentItem.previousSibling; this.ensureElementIsVisible(previousElement); } } this._suppressOnSelect = suppressSelect; // XXX actually it's just a refresh, but at least // the Extensions manager expects this: this._fireOnSelect(); return; } // try to restore the selected items according to their IDs // (applies after a template rebuild, if last-selected was not set) if (this.selectedItems) { let itemIds = []; for (let i = this.selectedCount - 1; i >= 0; i--) { let selectedItem = this.selectedItems[i]; itemIds.push(selectedItem.id); this.selectedItems.remove(selectedItem); } for (let i = 0; i < itemIds.length; i++) { let selectedItem = document.getElementById(itemIds[i]); if (selectedItem) { this.selectedItems.append(selectedItem); } } } if (this.currentItem && this.currentItem.id) this.currentItem = document.getElementById(this.currentItem.id); else this.currentItem = null; // if we have no previously current item or if the above check fails to // find the previous nodes (which causes it to clear selection) if (!this.currentItem && this.selectedCount == 0) { this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0; // cf. listbox constructor: // select items according to their attributes var children = this.children; for (let i = 0; i < children.length; ++i) { if (children[i].getAttribute("selected") == "true") this.selectedItems.append(children[i]); } } if (this.selType != "multiple" && this.selectedCount == 0) this.selectedItem = this.currentItem; ]]> </body> </method> <method name="_isItemVisible"> <parameter name="aItem"/> <body> <![CDATA[ if (!aItem) return false; var y = this.scrollBoxObject.positionY + this.scrollBoxObject.y; // Partially visible items are also considered visible return (aItem.boxObject.y + aItem.boxObject.height > y) && (aItem.boxObject.y < y + this.scrollBoxObject.height); ]]> </body> </method> <field name="_currentIndex">null</field> <!-- For backwards-compatibility and for convenience. Use getIndexOfItem instead. --> <method name="getIndexOf"> <parameter name="aElement"/> <body> <![CDATA[ return this.getIndexOfItem(aElement); ]]> </body> </method> <!-- For backwards-compatibility and for convenience. Use ensureElementIsVisible instead --> <method name="ensureSelectedElementIsVisible"> <body> <![CDATA[ return this.ensureElementIsVisible(this.selectedItem); ]]> </body> </method> <!-- For backwards-compatibility and for convenience. Use moveByOffset instead. --> <method name="goUp"> <body> <![CDATA[ var index = this.currentIndex; this.moveByOffset(-1, true, false); return index != this.currentIndex; ]]> </body> </method> <method name="goDown"> <body> <![CDATA[ var index = this.currentIndex; this.moveByOffset(1, true, false); return index != this.currentIndex; ]]> </body> </method> <!-- deprecated (is implied by currentItem and selectItem) --> <method name="fireActiveItemEvent"><body/></method> </implementation> <handlers> <handler event="click"> <![CDATA[ // clicking into nothing should unselect if (event.originalTarget == this._scrollbox) { this.clearSelection(); this.currentItem = null; } ]]> </handler> <handler event="MozSwipeGesture"> <![CDATA[ // Only handle swipe gestures up and down switch (event.direction) { case event.DIRECTION_DOWN: this._scrollbox.scrollTop = this._scrollbox.scrollHeight; break; case event.DIRECTION_UP: this._scrollbox.scrollTop = 0; break; } ]]> </handler> </handlers> </binding> <binding id="richlistitem" extends="chrome://global/content/bindings/listbox.xml#listitem"> <content> <children/> </content> <resources> <stylesheet src="chrome://global/skin/richlistbox.css"/> </resources> <implementation> <destructor> <![CDATA[ var control = this.control; if (!control) return; // When we are destructed and we are current or selected, unselect ourselves // so that richlistbox's selection doesn't point to something not in the DOM. // We don't want to reset last-selected, so we set _suppressOnSelect. if (this.selected) { var suppressSelect = control._suppressOnSelect; control._suppressOnSelect = true; control.removeItemFromSelection(this); control._suppressOnSelect = suppressSelect; } if (this.current) control.currentItem = null; ]]> </destructor> <property name="label" readonly="true"> <!-- Setter purposely not implemented; the getter returns a concatentation of label text to expose via accessibility APIs --> <getter> <![CDATA[ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; return Array.map(this.getElementsByTagNameNS(XULNS, "label"), label => label.value) .join(" "); ]]> </getter> </property> <property name="searchLabel"> <getter> <![CDATA[ return this.hasAttribute("searchlabel") ? this.getAttribute("searchlabel") : this.label; ]]> </getter> <setter> <![CDATA[ if (val !== null) this.setAttribute("searchlabel", val); else // fall back to the label property (default value) this.removeAttribute("searchlabel"); return val; ]]> </setter> </property> </implementation> </binding> </bindings>