/* 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() {
// 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", () => {
if (!
this.disabled) {
this.control.selectedItem =
this;
}
});
this.addEventListener(
"mousedown", () => {
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);
})();