summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/richlistbox.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/richlistbox.js')
-rw-r--r--toolkit/content/widgets/richlistbox.js1042
1 files changed, 1042 insertions, 0 deletions
diff --git a/toolkit/content/widgets/richlistbox.js b/toolkit/content/widgets/richlistbox.js
new file mode 100644
index 0000000000..0278d665f5
--- /dev/null
+++ b/toolkit/content/widgets/richlistbox.js
@@ -0,0 +1,1042 @@
+/* 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/. */
+"use strict";
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ /**
+ * XUL:richlistbox element.
+ */
+ MozElements.RichListBox = class RichListBox extends MozElements.BaseControl {
+ constructor() {
+ super();
+
+ this.selectedItems = new ChromeNodeList();
+ this._currentIndex = null;
+ this._lastKeyTime = 0;
+ this._incrementalString = "";
+ this._suppressOnSelect = false;
+ this._userSelecting = false;
+ this._selectTimeout = null;
+ this._currentItem = null;
+ this._selectionStart = null;
+
+ this.addEventListener(
+ "keypress",
+ event => {
+ if (event.altKey || event.metaKey) {
+ return;
+ }
+
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_UP:
+ this._moveByOffsetFromUserEvent(-1, event);
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ this._moveByOffsetFromUserEvent(1, event);
+ break;
+ case KeyEvent.DOM_VK_HOME:
+ this._moveByOffsetFromUserEvent(-this.currentIndex, event);
+ break;
+ case KeyEvent.DOM_VK_END:
+ this._moveByOffsetFromUserEvent(
+ this.getRowCount() - this.currentIndex - 1,
+ event
+ );
+ break;
+ case KeyEvent.DOM_VK_PAGE_UP:
+ this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
+ break;
+ case KeyEvent.DOM_VK_PAGE_DOWN:
+ this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
+ break;
+ }
+ },
+ { mozSystemGroup: true }
+ );
+
+ this.addEventListener("keypress", event => {
+ if (event.target != this) {
+ return;
+ }
+
+ if (
+ event.key == " " &&
+ event.ctrlKey &&
+ !event.shiftKey &&
+ !event.altKey &&
+ !event.metaKey &&
+ this.currentItem &&
+ this.selType == "multiple"
+ ) {
+ this.toggleItemSelection(this.currentItem);
+ }
+
+ if (!event.charCode || event.altKey || event.ctrlKey || event.metaKey) {
+ return;
+ }
+
+ if (event.timeStamp - this._lastKeyTime > 1000) {
+ this._incrementalString = "";
+ }
+
+ var key = String.fromCharCode(event.charCode).toLowerCase();
+ this._incrementalString += key;
+ this._lastKeyTime = event.timeStamp;
+
+ // If all letters in the incremental string are the same, just
+ // try to match the first one
+ var incrementalString = /^(.)\1+$/.test(this._incrementalString)
+ ? RegExp.$1
+ : this._incrementalString;
+ var length = incrementalString.length;
+
+ var rowCount = this.getRowCount();
+ var l = this.selectedItems.length;
+ var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
+ // start from the first element if none was selected or from the one
+ // following the selected one if it's a new or a repeated-letter search
+ if (start == -1 || length == 1) {
+ start++;
+ }
+
+ for (var i = 0; i < rowCount; i++) {
+ var k = (start + i) % rowCount;
+ var listitem = this.getItemAtIndex(k);
+ if (!this._canUserSelect(listitem)) {
+ continue;
+ }
+ // allow richlistitems to specify the string being searched for
+ var searchText =
+ "searchLabel" in listitem
+ ? listitem.searchLabel
+ : listitem.getAttribute("label"); // (see also bug 250123)
+ searchText = searchText.substring(0, length).toLowerCase();
+ if (searchText == incrementalString) {
+ this.ensureIndexIsVisible(k);
+ this.timedSelect(listitem, this._selectDelay);
+ break;
+ }
+ }
+ });
+
+ this.addEventListener("focus", event => {
+ if (this.getRowCount() > 0) {
+ if (this.currentIndex == -1) {
+ this.currentIndex = this.getIndexOfFirstVisibleRow();
+ let currentItem = this.getItemAtIndex(this.currentIndex);
+ if (currentItem) {
+ this.selectItem(currentItem);
+ }
+ } else {
+ this._fireEvent(this.currentItem, "DOMMenuItemActive");
+ }
+ }
+ this._lastKeyTime = 0;
+ });
+
+ this.addEventListener("click", event => {
+ // clicking into nothing should unselect multiple selections
+ if (event.originalTarget == this && this.selType == "multiple") {
+ this.clearSelection();
+ this.currentItem = null;
+ }
+ });
+
+ this.addEventListener("MozSwipeGesture", event => {
+ // Only handle swipe gestures up and down
+ switch (event.direction) {
+ case event.DIRECTION_DOWN:
+ this.scrollTop = this.scrollHeight;
+ break;
+ case event.DIRECTION_UP:
+ this.scrollTop = 0;
+ break;
+ }
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.setAttribute("allowevents", "true");
+ this._refreshSelection();
+ }
+
+ // nsIDOMXULSelectControlElement
+ set selectedItem(val) {
+ this.selectItem(val);
+ }
+ get selectedItem() {
+ return this.selectedItems.length ? this.selectedItems[0] : null;
+ }
+
+ // nsIDOMXULSelectControlElement
+ set selectedIndex(val) {
+ if (val >= 0) {
+ // This is a micro-optimization so that a call to getIndexOfItem or
+ // getItemAtIndex caused by _fireOnSelect (especially for derived
+ // widgets) won't loop the children.
+ this._selecting = {
+ item: this.getItemAtIndex(val),
+ index: val,
+ };
+ this.selectItem(this._selecting.item);
+ delete this._selecting;
+ } else {
+ this.clearSelection();
+ this.currentItem = null;
+ }
+ }
+ get selectedIndex() {
+ if (this.selectedItems.length) {
+ return this.getIndexOfItem(this.selectedItems[0]);
+ }
+ return -1;
+ }
+
+ // nsIDOMXULSelectControlElement
+ set value(val) {
+ var kids = this.getElementsByAttribute("value", val);
+ if (kids && kids.item(0)) {
+ this.selectItem(kids[0]);
+ }
+ }
+ get value() {
+ if (this.selectedItems.length) {
+ return this.selectedItem.value;
+ }
+ return null;
+ }
+
+ // nsIDOMXULSelectControlElement
+ get itemCount() {
+ return this.itemChildren.length;
+ }
+
+ // nsIDOMXULSelectControlElement
+ set selType(val) {
+ this.setAttribute("seltype", val);
+ }
+ get selType() {
+ return this.getAttribute("seltype");
+ }
+
+ // nsIDOMXULSelectControlElement
+ set currentItem(val) {
+ if (this._currentItem == val) {
+ return;
+ }
+
+ if (this._currentItem) {
+ this._currentItem.current = false;
+ if (!val && !this.suppressMenuItemEvent) {
+ // An item is losing focus and there is no new item to focus.
+ // Notify a11y that there is no focused item.
+ this._fireEvent(this._currentItem, "DOMMenuItemInactive");
+ }
+ }
+ this._currentItem = val;
+
+ if (val) {
+ val.current = true;
+ if (!this.suppressMenuItemEvent) {
+ // Notify a11y that this item got focus.
+ this._fireEvent(val, "DOMMenuItemActive");
+ }
+ }
+ }
+ get currentItem() {
+ return this._currentItem;
+ }
+
+ // nsIDOMXULSelectControlElement
+ set currentIndex(val) {
+ if (val >= 0) {
+ this.currentItem = this.getItemAtIndex(val);
+ } else {
+ this.currentItem = null;
+ }
+ }
+ get currentIndex() {
+ return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
+ }
+
+ // nsIDOMXULSelectControlElement
+ get selectedCount() {
+ return this.selectedItems.length;
+ }
+
+ get itemChildren() {
+ let children = Array.from(this.children).filter(
+ node => node.localName == "richlistitem"
+ );
+ return children;
+ }
+
+ set suppressOnSelect(val) {
+ this.setAttribute("suppressonselect", val);
+ }
+ get suppressOnSelect() {
+ return this.getAttribute("suppressonselect") == "true";
+ }
+
+ set _selectDelay(val) {
+ this.setAttribute("_selectDelay", val);
+ }
+ get _selectDelay() {
+ return this.getAttribute("_selectDelay") || 50;
+ }
+
+ _fireOnSelect() {
+ // 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");
+ }
+
+ getNextItem(aStartItem, aDelta) {
+ while (aStartItem) {
+ aStartItem = aStartItem.nextSibling;
+ if (
+ aStartItem &&
+ aStartItem.localName == "richlistitem" &&
+ (!this._userSelecting || this._canUserSelect(aStartItem))
+ ) {
+ --aDelta;
+ if (aDelta == 0) {
+ return aStartItem;
+ }
+ }
+ }
+ return null;
+ }
+
+ getPreviousItem(aStartItem, aDelta) {
+ while (aStartItem) {
+ aStartItem = aStartItem.previousSibling;
+ if (
+ aStartItem &&
+ aStartItem.localName == "richlistitem" &&
+ (!this._userSelecting || this._canUserSelect(aStartItem))
+ ) {
+ --aDelta;
+ if (aDelta == 0) {
+ return aStartItem;
+ }
+ }
+ }
+ return null;
+ }
+
+ appendItem(aLabel, aValue) {
+ var item = this.ownerDocument.createXULElement("richlistitem");
+ item.setAttribute("value", aValue);
+
+ var label = this.ownerDocument.createXULElement("label");
+ label.setAttribute("value", aLabel);
+ label.setAttribute("flex", "1");
+ label.setAttribute("crop", "end");
+ item.appendChild(label);
+
+ this.appendChild(item);
+
+ return item;
+ }
+
+ // nsIDOMXULSelectControlElement
+ getIndexOfItem(aItem) {
+ // don't search the children, if we're looking for none of them
+ if (aItem == null) {
+ return -1;
+ }
+ if (this._selecting && this._selecting.item == aItem) {
+ return this._selecting.index;
+ }
+ return this.itemChildren.indexOf(aItem);
+ }
+
+ // nsIDOMXULSelectControlElement
+ getItemAtIndex(aIndex) {
+ if (this._selecting && this._selecting.index == aIndex) {
+ return this._selecting.item;
+ }
+ return this.itemChildren[aIndex] || null;
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ addItemToSelection(aItem) {
+ if (this.selType != "multiple" && this.selectedCount) {
+ return;
+ }
+
+ if (aItem.selected) {
+ return;
+ }
+
+ this.selectedItems.append(aItem);
+ aItem.selected = true;
+
+ this._fireOnSelect();
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ removeItemFromSelection(aItem) {
+ if (!aItem.selected) {
+ return;
+ }
+
+ this.selectedItems.remove(aItem);
+ aItem.selected = false;
+ this._fireOnSelect();
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ toggleItemSelection(aItem) {
+ if (aItem.selected) {
+ this.removeItemFromSelection(aItem);
+ } else {
+ this.addItemToSelection(aItem);
+ }
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ selectItem(aItem) {
+ if (!aItem || aItem.disabled) {
+ return;
+ }
+
+ if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem) {
+ return;
+ }
+
+ this._selectionStart = null;
+
+ var suppress = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ this.clearSelection();
+ this.addItemToSelection(aItem);
+ this.currentItem = aItem;
+
+ this._suppressOnSelect = suppress;
+ this._fireOnSelect();
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ selectItemRange(aStartItem, aEndItem) {
+ if (this.selType != "multiple") {
+ return;
+ }
+
+ if (!aStartItem) {
+ aStartItem = this._selectionStart
+ ? this._selectionStart
+ : this.currentItem;
+ }
+
+ if (!aStartItem) {
+ aStartItem = aEndItem;
+ }
+
+ var suppressSelect = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ this._selectionStart = aStartItem;
+
+ var currentItem;
+ var startIndex = this.getIndexOfItem(aStartItem);
+ var endIndex = this.getIndexOfItem(aEndItem);
+ if (endIndex < startIndex) {
+ currentItem = aEndItem;
+ aEndItem = aStartItem;
+ aStartItem = currentItem;
+ } else {
+ currentItem = aStartItem;
+ }
+
+ while (currentItem) {
+ this.addItemToSelection(currentItem);
+ if (currentItem == aEndItem) {
+ currentItem = this.getNextItem(currentItem, 1);
+ break;
+ }
+ currentItem = this.getNextItem(currentItem, 1);
+ }
+
+ // Clear around new selection
+ // Don't use clearSelection() because it causes a lot of noise
+ // with respect to selection removed notifications used by the
+ // accessibility API support.
+ var userSelecting = this._userSelecting;
+ this._userSelecting = false; // that's US automatically unselecting
+ for (; currentItem; currentItem = this.getNextItem(currentItem, 1)) {
+ this.removeItemFromSelection(currentItem);
+ }
+
+ for (
+ currentItem = this.getItemAtIndex(0);
+ currentItem != aStartItem;
+ currentItem = this.getNextItem(currentItem, 1)
+ ) {
+ this.removeItemFromSelection(currentItem);
+ }
+ this._userSelecting = userSelecting;
+
+ this._suppressOnSelect = suppressSelect;
+
+ this._fireOnSelect();
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ selectAll() {
+ this._selectionStart = null;
+
+ var suppress = this._suppressOnSelect;
+ this._suppressOnSelect = true;
+
+ var item = this.getItemAtIndex(0);
+ while (item) {
+ this.addItemToSelection(item);
+ item = this.getNextItem(item, 1);
+ }
+
+ this._suppressOnSelect = suppress;
+ this._fireOnSelect();
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ clearSelection() {
+ if (this.selectedItems) {
+ while (this.selectedItems.length) {
+ let item = this.selectedItems[0];
+ item.selected = false;
+ this.selectedItems.remove(item);
+ }
+ }
+
+ this._selectionStart = null;
+ this._fireOnSelect();
+ }
+
+ // nsIDOMXULMultiSelectControlElement
+ getSelectedItem(aIndex) {
+ return aIndex < this.selectedItems.length
+ ? this.selectedItems[aIndex]
+ : null;
+ }
+
+ ensureIndexIsVisible(aIndex) {
+ return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
+ }
+
+ ensureElementIsVisible(aElement, aAlignToTop) {
+ if (!aElement) {
+ return;
+ }
+
+ // These calculations assume that there is no padding on the
+ // "richlistbox" element, although there might be a margin.
+ var targetRect = aElement.getBoundingClientRect();
+ var scrollRect = this.getBoundingClientRect();
+ var offset = targetRect.top - scrollRect.top;
+ if (!aAlignToTop && offset >= 0) {
+ // scrollRect.bottom wouldn't take a horizontal scroll bar into account
+ let scrollRectBottom = scrollRect.top + this.clientHeight;
+ offset = targetRect.bottom - scrollRectBottom;
+ if (offset <= 0) {
+ return;
+ }
+ }
+ this.scrollTop += offset;
+ }
+
+ getIndexOfFirstVisibleRow() {
+ var children = this.itemChildren;
+
+ for (var ix = 0; ix < children.length; ix++) {
+ if (this._isItemVisible(children[ix])) {
+ return ix;
+ }
+ }
+
+ return -1;
+ }
+
+ getRowCount() {
+ return this.itemChildren.length;
+ }
+
+ scrollOnePage(aDirection) {
+ var children = this.itemChildren;
+
+ if (!children.length) {
+ 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.
+ let height = this.getBoundingClientRect().height;
+ if (this._isItemVisible(this.currentItem)) {
+ this.scrollBy(0, 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
+ let currentItemRect = this.currentItem.getBoundingClientRect();
+ var startBorder = currentItemRect.y;
+ if (aDirection == -1) {
+ startBorder += currentItemRect.height;
+ }
+
+ var index = this.currentIndex;
+ for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
+ let childRect = children[ix].getBoundingClientRect();
+ if (childRect.height == 0) {
+ continue; // hidden children have a y of 0
+ }
+ var endBorder = childRect.y + (aDirection == -1 ? childRect.height : 0);
+ if ((endBorder - startBorder) * aDirection > height) {
+ break; // we've reached the desired distance
+ }
+ index = ix;
+ }
+
+ return index != this.currentIndex
+ ? index - this.currentIndex
+ : aDirection;
+ }
+
+ _refreshSelection() {
+ // 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.getBoundingClientRect().height) {
+ this.ensureElementIsVisible(currentItem);
+ } else {
+ // XXX hack around a bug in ensureElementIsVisible as it will
+ // scroll beyond the last element, bug 493645.
+ this.ensureElementIsVisible(currentItem.previousElementSibling);
+ }
+ }
+ 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.itemChildren;
+ 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;
+ }
+ }
+
+ _isItemVisible(aItem) {
+ if (!aItem) {
+ return false;
+ }
+
+ var y = this.getBoundingClientRect().y;
+
+ // Partially visible items are also considered visible
+ let itemRect = aItem.getBoundingClientRect();
+ return (
+ itemRect.y + itemRect.height > y && itemRect.y < y + this.clientHeight
+ );
+ }
+
+ moveByOffset(aOffset, aIsSelecting, aIsSelectingRange, aEvent) {
+ if ((aIsSelectingRange || !aIsSelecting) && this.selType != "multiple") {
+ return;
+ }
+
+ var newIndex = this.currentIndex + aOffset;
+ if (newIndex < 0) {
+ newIndex = 0;
+ }
+
+ var numItems = this.getRowCount();
+ if (newIndex > numItems - 1) {
+ newIndex = numItems - 1;
+ }
+
+ var newItem = this.getItemAtIndex(newIndex);
+ // make sure that the item is actually visible/selectable
+ if (this._userSelecting && newItem && !this._canUserSelect(newItem)) {
+ newItem =
+ aOffset > 0
+ ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1)
+ : this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
+ }
+ if (newItem) {
+ let hadFocus = this.currentItem.contains(document.activeElement);
+ this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
+ if (aIsSelectingRange) {
+ this.selectItemRange(null, newItem);
+ } else if (aIsSelecting) {
+ this.selectItem(newItem);
+ }
+ if (hadFocus) {
+ let flags =
+ Services.focus[
+ aEvent.type.startsWith("key") ? "FLAG_BYKEY" : "FLAG_BYJS"
+ ];
+ Services.focus.moveFocus(
+ window,
+ newItem,
+ Services.focus.MOVEFOCUS_FIRST,
+ flags
+ );
+ }
+
+ this.currentItem = newItem;
+ }
+ }
+
+ _moveByOffsetFromUserEvent(aOffset, aEvent) {
+ if (!aEvent.defaultPrevented) {
+ this._userSelecting = true;
+ this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey, aEvent);
+ this._userSelecting = false;
+ aEvent.preventDefault();
+ }
+ }
+
+ _canUserSelect(aItem) {
+ var style = document.defaultView.getComputedStyle(aItem);
+ return (
+ style.display != "none" &&
+ style.visibility == "visible" &&
+ style.MozUserInput != "none"
+ );
+ }
+
+ _selectTimeoutHandler(aMe) {
+ aMe._fireOnSelect();
+ aMe._selectTimeout = null;
+ }
+
+ timedSelect(aItem, aTimeout) {
+ var suppress = this._suppressOnSelect;
+ if (aTimeout != -1) {
+ this._suppressOnSelect = true;
+ }
+
+ this.selectItem(aItem);
+
+ this._suppressOnSelect = suppress;
+
+ if (aTimeout != -1) {
+ if (this._selectTimeout) {
+ window.clearTimeout(this._selectTimeout);
+ }
+ this._selectTimeout = window.setTimeout(
+ this._selectTimeoutHandler,
+ aTimeout,
+ this
+ );
+ }
+ }
+
+ /**
+ * For backwards-compatibility and for convenience.
+ * Use ensureElementIsVisible instead
+ */
+ ensureSelectedElementIsVisible() {
+ return this.ensureElementIsVisible(this.selectedItem);
+ }
+
+ _fireEvent(aTarget, aName) {
+ let event = document.createEvent("Events");
+ event.initEvent(aName, true, true);
+ aTarget.dispatchEvent(event);
+ }
+ };
+
+ MozXULElement.implementCustomInterface(MozElements.RichListBox, [
+ Ci.nsIDOMXULSelectControlElement,
+ Ci.nsIDOMXULMultiSelectControlElement,
+ ]);
+
+ customElements.define("richlistbox", MozElements.RichListBox);
+
+ /**
+ * XUL:richlistitem element.
+ */
+ MozElements.MozRichlistitem = class MozRichlistitem extends MozElements.BaseText {
+ constructor() {
+ super();
+
+ this.selectedByMouseOver = false;
+
+ /**
+ * If there is no modifier key, we select on mousedown, not
+ * click, so that drags work correctly.
+ */
+ this.addEventListener("mousedown", event => {
+ var control = this.control;
+ if (!control || control.disabled) {
+ return;
+ }
+ if (
+ (!event.ctrlKey ||
+ (AppConstants.platform == "macosx" && event.button == 2)) &&
+ !event.shiftKey &&
+ !event.metaKey
+ ) {
+ if (!this.selected) {
+ control.selectItem(this);
+ }
+ control.currentItem = this;
+ }
+ });
+
+ /**
+ * On a click (up+down on the same item), deselect everything
+ * except this item.
+ */
+ this.addEventListener("click", event => {
+ if (event.button != 0) {
+ return;
+ }
+
+ var control = this.control;
+ if (!control || control.disabled) {
+ return;
+ }
+ control._userSelecting = true;
+ if (control.selType != "multiple") {
+ control.selectItem(this);
+ } else if (event.ctrlKey || event.metaKey) {
+ control.toggleItemSelection(this);
+ control.currentItem = this;
+ } else if (event.shiftKey) {
+ control.selectItemRange(null, this);
+ control.currentItem = this;
+ } else {
+ /* We want to deselect all the selected items except what was
+ clicked, UNLESS it was a right-click. We have to do this
+ in click rather than mousedown so that you can drag a
+ selected group of items */
+
+ // use selectItemRange instead of selectItem, because this
+ // doesn't de- and reselect this item if it is selected
+ control.selectItemRange(this, this);
+ }
+ control._userSelecting = false;
+ });
+ }
+
+ connectedCallback() {
+ this._updateInnerControlsForSelection(this.selected);
+ }
+
+ /**
+ * nsIDOMXULSelectControlItemElement
+ */
+ get label() {
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return Array.from(
+ this.getElementsByTagNameNS(XUL_NS, "label"),
+ label => label.value
+ ).join(" ");
+ }
+
+ set searchLabel(val) {
+ if (val !== null) {
+ this.setAttribute("searchlabel", val);
+ }
+ // fall back to the label property (default value)
+ else {
+ this.removeAttribute("searchlabel");
+ }
+ }
+
+ get searchLabel() {
+ return this.hasAttribute("searchlabel")
+ ? this.getAttribute("searchlabel")
+ : this.label;
+ }
+ /**
+ * nsIDOMXULSelectControlItemElement
+ */
+ set value(val) {
+ this.setAttribute("value", val);
+ }
+
+ get value() {
+ return this.getAttribute("value");
+ }
+
+ /**
+ * nsIDOMXULSelectControlItemElement
+ */
+ set selected(val) {
+ if (val) {
+ this.setAttribute("selected", "true");
+ } else {
+ this.removeAttribute("selected");
+ }
+ this._updateInnerControlsForSelection(val);
+ }
+
+ get selected() {
+ return this.getAttribute("selected") == "true";
+ }
+ /**
+ * nsIDOMXULSelectControlItemElement
+ */
+ get control() {
+ var parent = this.parentNode;
+ while (parent) {
+ if (parent.localName == "richlistbox") {
+ return parent;
+ }
+ parent = parent.parentNode;
+ }
+ return null;
+ }
+
+ set current(val) {
+ if (val) {
+ this.setAttribute("current", "true");
+ } else {
+ this.removeAttribute("current");
+ }
+ }
+
+ get current() {
+ return this.getAttribute("current") == "true";
+ }
+
+ _updateInnerControlsForSelection(selected) {
+ for (let control of this.querySelectorAll("button,menulist")) {
+ if (!selected && control.tabIndex == 0) {
+ control.tabIndex = -1;
+ } else if (selected && control.tabIndex == -1) {
+ control.tabIndex = 0;
+ }
+ }
+ }
+ };
+
+ MozXULElement.implementCustomInterface(MozElements.MozRichlistitem, [
+ Ci.nsIDOMXULSelectControlItemElement,
+ ]);
+
+ customElements.define("richlistitem", MozElements.MozRichlistitem);
+}