зеркало из https://github.com/mozilla/gecko-dev.git
568 строки
15 KiB
JavaScript
568 строки
15 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.
|
|
(() => {
|
|
class MozRadiogroup extends MozElements.BaseControl {
|
|
constructor() {
|
|
super();
|
|
|
|
this.addEventListener("mousedown", event => {
|
|
if (this.disabled) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* keyboard navigation Here's how keyboard navigation works in radio groups on Windows:
|
|
* The group takes 'focus'
|
|
* The user is then free to navigate around inside the group
|
|
* using the arrow keys. Accessing previous or following radio buttons
|
|
* is done solely through the arrow keys and not the tab button. Tab
|
|
* takes you to the next widget in the tab order
|
|
*/
|
|
this.addEventListener("keypress", event => {
|
|
if (event.key != " " || event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.selectedItem = this.focusedItem;
|
|
this.selectedItem.doCommand();
|
|
// Prevent page from scrolling on the space key.
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", event => {
|
|
if (
|
|
event.keyCode != KeyEvent.DOM_VK_UP ||
|
|
event.originalTarget != this
|
|
) {
|
|
return;
|
|
}
|
|
this.checkAdjacentElement(false);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", event => {
|
|
if (
|
|
event.keyCode != KeyEvent.DOM_VK_LEFT ||
|
|
event.originalTarget != this
|
|
) {
|
|
return;
|
|
}
|
|
// left arrow goes back when we are ltr, forward when we are rtl
|
|
this.checkAdjacentElement(
|
|
document.defaultView.getComputedStyle(this).direction == "rtl"
|
|
);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", event => {
|
|
if (
|
|
event.keyCode != KeyEvent.DOM_VK_DOWN ||
|
|
event.originalTarget != this
|
|
) {
|
|
return;
|
|
}
|
|
this.checkAdjacentElement(true);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.addEventListener("keypress", event => {
|
|
if (
|
|
event.keyCode != KeyEvent.DOM_VK_RIGHT ||
|
|
event.originalTarget != this
|
|
) {
|
|
return;
|
|
}
|
|
// right arrow goes forward when we are ltr, back when we are rtl
|
|
this.checkAdjacentElement(
|
|
document.defaultView.getComputedStyle(this).direction == "ltr"
|
|
);
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
});
|
|
|
|
/**
|
|
* set a focused attribute on the selected item when the group
|
|
* receives focus so that we can style it as if it were focused even though
|
|
* it is not (Windows platform behaviour is for the group to receive focus,
|
|
* not the item
|
|
*/
|
|
this.addEventListener("focus", event => {
|
|
if (event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.setAttribute("focused", "true");
|
|
if (this.focusedItem) {
|
|
return;
|
|
}
|
|
|
|
var val = this.selectedItem;
|
|
if (!val || val.disabled || val.hidden || val.collapsed) {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (
|
|
!children[i].hidden &&
|
|
!children[i].collapsed &&
|
|
!children[i].disabled
|
|
) {
|
|
val = children[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.focusedItem = val;
|
|
});
|
|
|
|
this.addEventListener("blur", event => {
|
|
if (event.originalTarget != this) {
|
|
return;
|
|
}
|
|
this.removeAttribute("focused");
|
|
this.focusedItem = null;
|
|
});
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
// When this is called via `connectedCallback` there are two main variations:
|
|
// 1) The radiogroup and radio children are defined in markup.
|
|
// 2) We are appending a DocumentFragment
|
|
// In both cases, the <radiogroup> connectedCallback fires first. But in (2),
|
|
// the children <radio>s won't be upgraded yet, so r.control will be undefined.
|
|
// To avoid churn in this case where we would have to reinitialize the list as each
|
|
// child radio gets upgraded as a result of init(), ignore the resulting calls
|
|
// to radioAttached.
|
|
this.ignoreRadioChildConstruction = true;
|
|
this.init();
|
|
this.ignoreRadioChildConstruction = false;
|
|
if (!this.value) {
|
|
this.selectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
init() {
|
|
this._radioChildren = null;
|
|
|
|
if (this.getAttribute("disabled") == "true") {
|
|
this.disabled = true;
|
|
}
|
|
|
|
var children = this._getRadioChildren();
|
|
var length = children.length;
|
|
for (var i = 0; i < length; i++) {
|
|
if (children[i].getAttribute("selected") == "true") {
|
|
this.selectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
|
|
var value = this.value;
|
|
if (value) {
|
|
this.value = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a new <radio> gets added to an already connected radiogroup.
|
|
* This can happen due to DOM getting appended after the <radiogroup> is created.
|
|
* When this happens, reinitialize the UI if necessary to make sure the state is
|
|
* consistent.
|
|
*
|
|
* @param {DOMNode} child
|
|
* The <radio> element that got added
|
|
*/
|
|
radioAttached(child) {
|
|
if (this.ignoreRadioChildConstruction) {
|
|
return;
|
|
}
|
|
if (!this._radioChildren || !this._radioChildren.includes(child)) {
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a new <radio> gets removed from a radio group.
|
|
*
|
|
* @param {DOMNode} child
|
|
* The <radio> element that got removed
|
|
*/
|
|
radioUnattached(child) {
|
|
// Just invalidate the cache, next time it's fetched it'll get rebuilt.
|
|
this._radioChildren = null;
|
|
}
|
|
|
|
set value(val) {
|
|
this.setAttribute("value", val);
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; i++) {
|
|
if (String(children[i].value) == String(val)) {
|
|
this.selectedItem = children[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
get value() {
|
|
return this.getAttribute("value");
|
|
}
|
|
|
|
set disabled(val) {
|
|
if (val) {
|
|
this.setAttribute("disabled", "true");
|
|
} else {
|
|
this.removeAttribute("disabled");
|
|
}
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
children[i].disabled = val;
|
|
}
|
|
}
|
|
|
|
get disabled() {
|
|
if (this.getAttribute("disabled") == "true") {
|
|
return true;
|
|
}
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (
|
|
!children[i].hidden &&
|
|
!children[i].collapsed &&
|
|
!children[i].disabled
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
get itemCount() {
|
|
return this._getRadioChildren().length;
|
|
}
|
|
|
|
set selectedIndex(val) {
|
|
this.selectedItem = this._getRadioChildren()[val];
|
|
}
|
|
|
|
get selectedIndex() {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i].selected) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
set selectedItem(val) {
|
|
var focused = this.getAttribute("focused") == "true";
|
|
var alreadySelected = false;
|
|
|
|
if (val) {
|
|
alreadySelected = val.getAttribute("selected") == "true";
|
|
val.setAttribute("focused", focused);
|
|
val.setAttribute("selected", "true");
|
|
this.setAttribute("value", val.value);
|
|
} else {
|
|
this.removeAttribute("value");
|
|
}
|
|
|
|
// uncheck all other group nodes
|
|
var children = this._getRadioChildren();
|
|
var previousItem = null;
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i] != val) {
|
|
if (children[i].getAttribute("selected") == "true") {
|
|
previousItem = children[i];
|
|
}
|
|
|
|
children[i].removeAttribute("selected");
|
|
children[i].removeAttribute("focused");
|
|
}
|
|
}
|
|
|
|
var event = document.createEvent("Events");
|
|
event.initEvent("select", false, true);
|
|
this.dispatchEvent(event);
|
|
|
|
if (focused) {
|
|
if (alreadySelected) {
|
|
// Notify accessibility that this item got focus.
|
|
event = document.createEvent("Events");
|
|
event.initEvent("DOMMenuItemActive", true, true);
|
|
val.dispatchEvent(event);
|
|
} else {
|
|
// Only report if actual change
|
|
if (val) {
|
|
// Accessibility will fire focus for this.
|
|
event = document.createEvent("Events");
|
|
event.initEvent("RadioStateChange", true, true);
|
|
val.dispatchEvent(event);
|
|
}
|
|
|
|
if (previousItem) {
|
|
event = document.createEvent("Events");
|
|
event.initEvent("RadioStateChange", true, true);
|
|
previousItem.dispatchEvent(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get selectedItem() {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i].selected) {
|
|
return children[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
set focusedItem(val) {
|
|
if (val) {
|
|
val.setAttribute("focused", "true");
|
|
// Notify accessibility that this item got focus.
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("DOMMenuItemActive", true, true);
|
|
val.dispatchEvent(event);
|
|
}
|
|
|
|
// unfocus all other group nodes
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i] != val) {
|
|
children[i].removeAttribute("focused");
|
|
}
|
|
}
|
|
}
|
|
|
|
get focusedItem() {
|
|
var children = this._getRadioChildren();
|
|
for (var i = 0; i < children.length; ++i) {
|
|
if (children[i].getAttribute("focused") == "true") {
|
|
return children[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
checkAdjacentElement(aNextFlag) {
|
|
var currentElement = this.focusedItem || this.selectedItem;
|
|
var i;
|
|
var children = this._getRadioChildren();
|
|
for (i = 0; i < children.length; ++i) {
|
|
if (children[i] == currentElement) {
|
|
break;
|
|
}
|
|
}
|
|
var index = i;
|
|
|
|
if (aNextFlag) {
|
|
do {
|
|
if (++i == children.length) {
|
|
i = 0;
|
|
}
|
|
if (i == index) {
|
|
break;
|
|
}
|
|
} while (
|
|
children[i].hidden ||
|
|
children[i].collapsed ||
|
|
children[i].disabled
|
|
);
|
|
// XXX check for display/visibility props too
|
|
|
|
this.selectedItem = children[i];
|
|
children[i].doCommand();
|
|
} else {
|
|
do {
|
|
if (i == 0) {
|
|
i = children.length;
|
|
}
|
|
if (--i == index) {
|
|
break;
|
|
}
|
|
} while (
|
|
children[i].hidden ||
|
|
children[i].collapsed ||
|
|
children[i].disabled
|
|
);
|
|
// XXX check for display/visibility props too
|
|
|
|
this.selectedItem = children[i];
|
|
children[i].doCommand();
|
|
}
|
|
}
|
|
|
|
_getRadioChildren() {
|
|
if (this._radioChildren) {
|
|
return this._radioChildren;
|
|
}
|
|
|
|
let radioChildren = [];
|
|
if (this.hasChildNodes()) {
|
|
for (let radio of this.querySelectorAll("radio")) {
|
|
customElements.upgrade(radio);
|
|
if (radio.control == this) {
|
|
radioChildren.push(radio);
|
|
}
|
|
}
|
|
} else {
|
|
const XUL_NS =
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
for (let radio of this.ownerDocument.getElementsByAttribute(
|
|
"group",
|
|
this.id
|
|
)) {
|
|
if (radio.namespaceURI == XUL_NS && radio.localName == "radio") {
|
|
customElements.upgrade(radio);
|
|
radioChildren.push(radio);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (this._radioChildren = radioChildren);
|
|
}
|
|
|
|
getIndexOfItem(item) {
|
|
return this._getRadioChildren().indexOf(item);
|
|
}
|
|
|
|
getItemAtIndex(index) {
|
|
var children = this._getRadioChildren();
|
|
return index >= 0 && index < children.length ? children[index] : null;
|
|
}
|
|
|
|
appendItem(label, value) {
|
|
var radio = document.createXULElement("radio");
|
|
radio.setAttribute("label", label);
|
|
radio.setAttribute("value", value);
|
|
this.appendChild(radio);
|
|
return radio;
|
|
}
|
|
}
|
|
|
|
MozXULElement.implementCustomInterface(MozRadiogroup, [
|
|
Ci.nsIDOMXULSelectControlElement,
|
|
Ci.nsIDOMXULRadioGroupElement,
|
|
]);
|
|
|
|
customElements.define("radiogroup", MozRadiogroup);
|
|
|
|
class MozRadio extends MozElements.BaseText {
|
|
static get markup() {
|
|
return `
|
|
<image class="radio-check"></image>
|
|
<hbox class="radio-label-box" align="center" flex="1">
|
|
<image class="radio-icon"></image>
|
|
<label class="radio-label" flex="1"></label>
|
|
</hbox>
|
|
`;
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
".radio-check": "disabled,selected",
|
|
".radio-label": "text=label,accesskey,crop",
|
|
".radio-icon": "src",
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.addEventListener("click", event => {
|
|
if (!this.disabled) {
|
|
this.control.selectedItem = this;
|
|
}
|
|
});
|
|
|
|
this.addEventListener("mousedown", event => {
|
|
if (!this.disabled) {
|
|
this.control.focusedItem = this;
|
|
}
|
|
});
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.delayConnectedCallback()) {
|
|
return;
|
|
}
|
|
|
|
if (!this.connectedOnce) {
|
|
this.connectedOnce = true;
|
|
// If the caller didn't provide custom content then append the default:
|
|
if (!this.firstElementChild) {
|
|
this.appendChild(this.constructor.fragment);
|
|
this.initializeAttributeInheritance();
|
|
}
|
|
}
|
|
|
|
var control = (this._control = this.control);
|
|
if (control) {
|
|
control.radioAttached(this);
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.control) {
|
|
this.control.radioUnattached(this);
|
|
}
|
|
this._control = null;
|
|
}
|
|
|
|
set value(val) {
|
|
this.setAttribute("value", val);
|
|
}
|
|
|
|
get value() {
|
|
return this.getAttribute("value");
|
|
}
|
|
|
|
get selected() {
|
|
return this.hasAttribute("selected");
|
|
}
|
|
|
|
get radioGroup() {
|
|
return this.control;
|
|
}
|
|
|
|
get control() {
|
|
if (this._control) {
|
|
return this._control;
|
|
}
|
|
|
|
var radiogroup = this.closest("radiogroup");
|
|
if (radiogroup) {
|
|
return radiogroup;
|
|
}
|
|
|
|
var group = this.getAttribute("group");
|
|
if (!group) {
|
|
return null;
|
|
}
|
|
|
|
var parent = this.ownerDocument.getElementById(group);
|
|
if (!parent || parent.localName != "radiogroup") {
|
|
parent = null;
|
|
}
|
|
return parent;
|
|
}
|
|
}
|
|
|
|
MozXULElement.implementCustomInterface(MozRadio, [
|
|
Ci.nsIDOMXULSelectControlItemElement,
|
|
]);
|
|
customElements.define("radio", MozRadio);
|
|
})();
|