fune/toolkit/content/widgets/richlistbox.js
Gijs Kruitbosch 7e7c04544c Bug 1329643 - implement generic richlistbox improvements for keyboard focus, r=Jamie,settings-reviewers,mossop
Rather than having each richlistbox consumer having to reinvent focus patterns for
buttons and menulists in its 'rich' items, let's just teach richlistbox and
richlistitem to not suck at keyboard navigation. That way we won't keep forgetting
to deal with this whenever we add new lists anywhere.

This allows us to remove the custom handling in sitePermissions.js, and the same
handling should be covered by the existing test, ie
browser/components/preferences/tests/browser_permissions_dialog.js

To summarize the desired keyboard behaviour:
- tab/shift-tab move focus to controls inside selected items only (not other rows)
- arrow keys move the list selection up/down
- when arrowing to move the list selection, focus moves with the selection if it
  was previously on a control in the previously selected item.

Differential Revision: https://phabricator.services.mozilla.com/D161528
2022-11-14 21:07:08 +00:00

1042 lines
29 KiB
JavaScript

/* 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);
}