summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/richlistbox.xml
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/richlistbox.xml')
-rw-r--r--toolkit/content/widgets/richlistbox.xml589
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>