summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/radio.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/radio.js')
-rw-r--r--toolkit/content/widgets/radio.js567
1 files changed, 567 insertions, 0 deletions
diff --git a/toolkit/content/widgets/radio.js b/toolkit/content/widgets/radio.js
new file mode 100644
index 0000000000..482323acb9
--- /dev/null
+++ b/toolkit/content/widgets/radio.js
@@ -0,0 +1,567 @@
+/* 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);
+})();