зеркало из https://github.com/mozilla/gecko-dev.git
295 строки
7.5 KiB
JavaScript
295 строки
7.5 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/. */
|
|
|
|
export function VoiceSelect(win, label) {
|
|
this._winRef = Cu.getWeakReference(win);
|
|
|
|
let element = win.document.createElement("div");
|
|
element.classList.add("voiceselect");
|
|
// eslint-disable-next-line no-unsanitized/property
|
|
element.innerHTML = `<button class="select-toggle" aria-controls="voice-options">
|
|
<span class="label">${label}</span> <span class="current-voice"></span>
|
|
</button>
|
|
<div class="options" id="voice-options" role="listbox"></div>`;
|
|
|
|
this._elementRef = Cu.getWeakReference(element);
|
|
|
|
let button = this.selectToggle;
|
|
button.addEventListener("click", this);
|
|
button.addEventListener("keydown", this);
|
|
|
|
let listbox = this.listbox;
|
|
listbox.addEventListener("click", this);
|
|
listbox.addEventListener("mousemove", this);
|
|
listbox.addEventListener("keydown", this);
|
|
listbox.addEventListener("wheel", this, true);
|
|
|
|
win.addEventListener("resize", () => {
|
|
this._updateDropdownHeight();
|
|
});
|
|
}
|
|
|
|
VoiceSelect.prototype = {
|
|
add(label, value) {
|
|
let option = this._doc.createElement("button");
|
|
option.dataset.value = value;
|
|
option.classList.add("option");
|
|
option.tabIndex = "-1";
|
|
option.setAttribute("role", "option");
|
|
option.textContent = label;
|
|
this.listbox.appendChild(option);
|
|
return option;
|
|
},
|
|
|
|
addOptions(options) {
|
|
let selected = null;
|
|
for (let option of options) {
|
|
if (option.selected) {
|
|
selected = this.add(option.label, option.value);
|
|
} else {
|
|
this.add(option.label, option.value);
|
|
}
|
|
}
|
|
|
|
this._select(selected || this.options[0], true);
|
|
},
|
|
|
|
clear() {
|
|
this.listbox.innerHTML = "";
|
|
},
|
|
|
|
toggleList(force, focus = true) {
|
|
if (this.element.classList.toggle("open", force)) {
|
|
if (focus) {
|
|
(this.selected || this.options[0]).focus();
|
|
}
|
|
|
|
this._updateDropdownHeight(true);
|
|
this.listbox.setAttribute("aria-expanded", true);
|
|
this._win.addEventListener("focus", this, true);
|
|
} else {
|
|
if (focus) {
|
|
this.element.querySelector(".select-toggle").focus();
|
|
}
|
|
|
|
this.listbox.setAttribute("aria-expanded", false);
|
|
this._win.removeEventListener("focus", this, true);
|
|
}
|
|
},
|
|
|
|
handleEvent(evt) {
|
|
let target = evt.target;
|
|
|
|
switch (evt.type) {
|
|
case "click":
|
|
target = target.closest(".option, .select-toggle") || target;
|
|
if (target.classList.contains("option")) {
|
|
if (!target.classList.contains("selected")) {
|
|
this.selected = target;
|
|
}
|
|
|
|
this.toggleList(false);
|
|
} else if (target.classList.contains("select-toggle")) {
|
|
this.toggleList();
|
|
}
|
|
break;
|
|
|
|
case "mousemove":
|
|
this.listbox.classList.add("hovering");
|
|
break;
|
|
|
|
case "keydown":
|
|
if (target.classList.contains("select-toggle")) {
|
|
if (evt.altKey) {
|
|
this.toggleList(true);
|
|
} else {
|
|
this._keyDownedButton(evt);
|
|
}
|
|
} else {
|
|
this.listbox.classList.remove("hovering");
|
|
this._keyDownedInBox(evt);
|
|
}
|
|
break;
|
|
|
|
case "wheel":
|
|
// Don't let wheel events bubble to document. It will scroll the page
|
|
// and close the entire narrate dialog.
|
|
evt.stopPropagation();
|
|
break;
|
|
|
|
case "focus":
|
|
if (!target.closest(".voiceselect")) {
|
|
this.toggleList(false, false);
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
_getPagedOption(option, up) {
|
|
let height = elem => elem.getBoundingClientRect().height;
|
|
let listboxHeight = height(this.listbox);
|
|
|
|
let next = option;
|
|
for (let delta = 0; delta < listboxHeight; delta += height(next)) {
|
|
let sibling = up ? next.previousElementSibling : next.nextElementSibling;
|
|
if (!sibling) {
|
|
break;
|
|
}
|
|
|
|
next = sibling;
|
|
}
|
|
|
|
return next;
|
|
},
|
|
|
|
_keyDownedButton(evt) {
|
|
if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) {
|
|
this.toggleList(true);
|
|
return;
|
|
}
|
|
|
|
let toSelect;
|
|
switch (evt.key) {
|
|
case "PageUp":
|
|
case "ArrowUp":
|
|
toSelect = this.selected.previousElementSibling;
|
|
break;
|
|
case "PageDown":
|
|
case "ArrowDown":
|
|
toSelect = this.selected.nextElementSibling;
|
|
break;
|
|
case "Home":
|
|
toSelect = this.selected.parentNode.firstElementChild;
|
|
break;
|
|
case "End":
|
|
toSelect = this.selected.parentNode.lastElementChild;
|
|
break;
|
|
}
|
|
|
|
if (toSelect && toSelect.classList.contains("option")) {
|
|
evt.preventDefault();
|
|
this.selected = toSelect;
|
|
}
|
|
},
|
|
|
|
_keyDownedInBox(evt) {
|
|
let toFocus;
|
|
let cur = this._doc.activeElement;
|
|
|
|
switch (evt.key) {
|
|
case "ArrowUp":
|
|
toFocus = cur.previousElementSibling || this.listbox.lastElementChild;
|
|
break;
|
|
case "ArrowDown":
|
|
toFocus = cur.nextElementSibling || this.listbox.firstElementChild;
|
|
break;
|
|
case "PageUp":
|
|
toFocus = this._getPagedOption(cur, true);
|
|
break;
|
|
case "PageDown":
|
|
toFocus = this._getPagedOption(cur, false);
|
|
break;
|
|
case "Home":
|
|
toFocus = cur.parentNode.firstElementChild;
|
|
break;
|
|
case "End":
|
|
toFocus = cur.parentNode.lastElementChild;
|
|
break;
|
|
case "Escape":
|
|
this.toggleList(false);
|
|
break;
|
|
}
|
|
|
|
if (toFocus && toFocus.classList.contains("option")) {
|
|
evt.preventDefault();
|
|
toFocus.focus();
|
|
}
|
|
},
|
|
|
|
_select(option, suppressEvent = false) {
|
|
let oldSelected = this.selected;
|
|
if (oldSelected) {
|
|
oldSelected.removeAttribute("aria-selected");
|
|
oldSelected.classList.remove("selected");
|
|
}
|
|
|
|
if (option) {
|
|
option.setAttribute("aria-selected", true);
|
|
option.classList.add("selected");
|
|
this.element.querySelector(".current-voice").textContent =
|
|
option.textContent;
|
|
}
|
|
|
|
if (!suppressEvent) {
|
|
let evt = this.element.ownerDocument.createEvent("Event");
|
|
evt.initEvent("change", true, true);
|
|
this.element.dispatchEvent(evt);
|
|
}
|
|
},
|
|
|
|
_updateDropdownHeight(now) {
|
|
let updateInner = () => {
|
|
let winHeight = this._win.innerHeight;
|
|
let listbox = this.listbox;
|
|
let listboxTop = listbox.getBoundingClientRect().top;
|
|
listbox.style.maxHeight = winHeight - listboxTop - 10 + "px";
|
|
};
|
|
|
|
if (now) {
|
|
updateInner();
|
|
} else if (!this._pendingDropdownUpdate) {
|
|
this._pendingDropdownUpdate = true;
|
|
this._win.requestAnimationFrame(() => {
|
|
updateInner();
|
|
delete this._pendingDropdownUpdate;
|
|
});
|
|
}
|
|
},
|
|
|
|
_getOptionFromValue(value) {
|
|
return Array.from(this.options).find(o => o.dataset.value === value);
|
|
},
|
|
|
|
get element() {
|
|
return this._elementRef.get();
|
|
},
|
|
|
|
get listbox() {
|
|
return this._elementRef.get().querySelector(".options");
|
|
},
|
|
|
|
get selectToggle() {
|
|
return this._elementRef.get().querySelector(".select-toggle");
|
|
},
|
|
|
|
get _win() {
|
|
return this._winRef.get();
|
|
},
|
|
|
|
get _doc() {
|
|
return this._win.document;
|
|
},
|
|
|
|
set selected(option) {
|
|
this._select(option);
|
|
},
|
|
|
|
get selected() {
|
|
return this.element.querySelector(".options > .option.selected");
|
|
},
|
|
|
|
get options() {
|
|
return this.element.querySelectorAll(".options > .option");
|
|
},
|
|
|
|
set value(value) {
|
|
this._select(this._getOptionFromValue(value));
|
|
},
|
|
|
|
get value() {
|
|
let selected = this.selected;
|
|
return selected ? selected.dataset.value : "";
|
|
},
|
|
};
|