diff options
Diffstat (limited to 'toolkit/content/widgets/richlistbox.xml')
-rw-r--r-- | toolkit/content/widgets/richlistbox.xml | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/toolkit/content/widgets/richlistbox.xml b/toolkit/content/widgets/richlistbox.xml new file mode 100644 index 000000000..dd04a0cff --- /dev/null +++ b/toolkit/content/widgets/richlistbox.xml @@ -0,0 +1,589 @@ +<?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> |