diff options
author | Matt A. Tobin <email@mattatobin.com> | 2021-11-14 01:30:12 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2021-11-14 01:30:12 -0500 |
commit | 43fa0a71c814e902165782680f2f9705e3c234c9 (patch) | |
tree | 0ab3c66e8432febc00c18c036706c7ff43c11799 /toolkit/components/narrate | |
parent | 2fccdffac3075ee72087f9ee153204b02c7d931a (diff) | |
download | aura-central-43fa0a71c814e902165782680f2f9705e3c234c9.tar.gz |
Issue %3005 - Move toolkit/components to components/
Diffstat (limited to 'toolkit/components/narrate')
-rw-r--r-- | toolkit/components/narrate/.eslintrc.js | 94 | ||||
-rw-r--r-- | toolkit/components/narrate/NarrateControls.jsm | 313 | ||||
-rw-r--r-- | toolkit/components/narrate/Narrator.jsm | 440 | ||||
-rw-r--r-- | toolkit/components/narrate/VoiceSelect.jsm | 300 | ||||
-rw-r--r-- | toolkit/components/narrate/moz.build | 10 |
5 files changed, 0 insertions, 1157 deletions
diff --git a/toolkit/components/narrate/.eslintrc.js b/toolkit/components/narrate/.eslintrc.js deleted file mode 100644 index b2d443575..000000000 --- a/toolkit/components/narrate/.eslintrc.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; - -module.exports = { // eslint-disable-line no-undef - "extends": [ - "../../.eslintrc.js" - ], - - "globals": { - "Components": true, - "dump": true, - "Iterator": true - }, - - "env": { "browser": true }, - - "rules": { - // Mozilla stuff - "mozilla/no-aArgs": "warn", - "mozilla/reject-importGlobalProperties": "warn", - "mozilla/var-only-at-top-level": "warn", - - "block-scoped-var": "error", - "brace-style": ["warn", "1tbs", {"allowSingleLine": false}], - "camelcase": "warn", - "comma-dangle": "off", - "comma-spacing": ["warn", {"before": false, "after": true}], - "comma-style": ["warn", "last"], - "complexity": "warn", - "consistent-return": "error", - "curly": "error", - "dot-location": ["warn", "property"], - "dot-notation": "error", - "eol-last": "error", - "generator-star-spacing": ["warn", "after"], - "indent": ["warn", 2, {"SwitchCase": 1}], - "key-spacing": ["warn", {"beforeColon": false, "afterColon": true}], - "keyword-spacing": "warn", - "max-len": ["warn", 80, 2, {"ignoreUrls": true}], - "max-nested-callbacks": ["error", 3], - "new-cap": ["error", {"capIsNew": false}], - "new-parens": "error", - "no-array-constructor": "error", - "no-cond-assign": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-args": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-else-return": "error", - "no-eval": "error", - "no-extend-native": "error", - "no-extra-bind": "error", - "no-extra-boolean-cast": "error", - "no-extra-semi": "warn", - "no-fallthrough": "error", - "no-inline-comments": "warn", - "no-lonely-if": "error", - "no-mixed-spaces-and-tabs": "error", - "no-multi-spaces": "warn", - "no-multi-str": "warn", - "no-multiple-empty-lines": ["warn", {"max": 1}], - "no-native-reassign": "error", - "no-nested-ternary": "error", - "no-redeclare": "error", - "no-return-assign": "error", - "no-self-compare": "error", - "no-sequences": "error", - "no-shadow": "warn", - "no-shadow-restricted-names": "error", - "no-spaced-func": "warn", - "no-throw-literal": "error", - "no-trailing-spaces": "error", - "no-undef": "error", - "no-unneeded-ternary": "error", - "no-unreachable": "error", - "no-unused-vars": "error", - "no-with": "error", - "padded-blocks": ["warn", "never"], - "quotes": ["warn", "double", "avoid-escape"], - "semi": ["warn", "always"], - "semi-spacing": ["warn", {"before": false, "after": true}], - "space-before-blocks": ["warn", "always"], - "space-before-function-paren": ["warn", "never"], - "space-in-parens": ["warn", "never"], - "space-infix-ops": ["warn", {"int32Hint": true}], - "space-unary-ops": ["warn", { "words": true, "nonwords": false }], - "spaced-comment": ["warn", "always"], - "strict": ["error", "global"], - "use-isnan": "error", - "valid-typeof": "error", - "yoda": "error" - } -}; diff --git a/toolkit/components/narrate/NarrateControls.jsm b/toolkit/components/narrate/NarrateControls.jsm deleted file mode 100644 index 56b3deaf8..000000000 --- a/toolkit/components/narrate/NarrateControls.jsm +++ /dev/null @@ -1,313 +0,0 @@ -/* 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"; - -const Cu = Components.utils; - -Cu.import("resource://gre/modules/narrate/VoiceSelect.jsm"); -Cu.import("resource://gre/modules/narrate/Narrator.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/AsyncPrefs.jsm"); - -this.EXPORTED_SYMBOLS = ["NarrateControls"]; - -var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties"); - -function NarrateControls(win, languagePromise) { - this._winRef = Cu.getWeakReference(win); - this._languagePromise = languagePromise; - - win.addEventListener("unload", this); - - // Append content style sheet in document head - let style = win.document.createElement("link"); - style.rel = "stylesheet"; - style.href = "chrome://global/skin/narrate.css"; - win.document.head.appendChild(style); - - function localize(pieces, ...substitutions) { - let result = pieces[0]; - for (let i = 0; i < substitutions.length; ++i) { - result += gStrings.GetStringFromName(substitutions[i]) + pieces[i + 1]; - } - return result; - } - - let dropdown = win.document.createElement("ul"); - dropdown.className = "dropdown narrate-dropdown"; - // We need inline svg here for the animation to work (bug 908634 & 1190881). - // eslint-disable-next-line no-unsanitized/property - dropdown.innerHTML = - localize`<li> - <button class="dropdown-toggle button narrate-toggle" - title="${"narrate"}" hidden> - <svg xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - width="24" height="24" viewBox="0 0 24 24"> - <style> - @keyframes grow { - 0% { transform: scaleY(1); } - 15% { transform: scaleY(1.5); } - 15% { transform: scaleY(1.5); } - 30% { transform: scaleY(1); } - 100% { transform: scaleY(1); } - } - - .waveform > rect { - fill: #808080; - } - - .speaking .waveform > rect { - fill: #58bf43; - transform-box: fill-box; - transform-origin: 50% 50%; - animation-name: grow; - animation-duration: 1750ms; - animation-iteration-count: infinite; - animation-timing-function: linear; - } - - .waveform > rect:nth-child(2) { animation-delay: 250ms; } - .waveform > rect:nth-child(3) { animation-delay: 500ms; } - .waveform > rect:nth-child(4) { animation-delay: 750ms; } - .waveform > rect:nth-child(5) { animation-delay: 1000ms; } - .waveform > rect:nth-child(6) { animation-delay: 1250ms; } - .waveform > rect:nth-child(7) { animation-delay: 1500ms; } - - </style> - <g class="waveform"> - <rect x="1" y="8" width="2" height="8" rx=".5" ry=".5" /> - <rect x="4" y="5" width="2" height="14" rx=".5" ry=".5" /> - <rect x="7" y="8" width="2" height="8" rx=".5" ry=".5" /> - <rect x="10" y="4" width="2" height="16" rx=".5" ry=".5" /> - <rect x="13" y="2" width="2" height="20" rx=".5" ry=".5" /> - <rect x="16" y="4" width="2" height="16" rx=".5" ry=".5" /> - <rect x="19" y="7" width="2" height="10" rx=".5" ry=".5" /> - </g> - </svg> - </button> - </li> - <li class="dropdown-popup"> - <div class="narrate-row narrate-control"> - <button disabled class="narrate-skip-previous" - title="${"back"}"></button> - <button class="narrate-start-stop" title="${"start"}"></button> - <button disabled class="narrate-skip-next" - title="${"forward"}"></button> - </div> - <div class="narrate-row narrate-rate"> - <input class="narrate-rate-input" value="0" title="${"speed"}" - step="5" max="100" min="-100" type="range"> - </div> - <div class="narrate-row narrate-voices"></div> - <div class="dropdown-arrow"></div> - </li>`; - - this.narrator = new Narrator(win, languagePromise); - - let branch = Services.prefs.getBranch("narrate."); - let selectLabel = gStrings.GetStringFromName("selectvoicelabel"); - this.voiceSelect = new VoiceSelect(win, selectLabel); - this.voiceSelect.element.addEventListener("change", this); - this.voiceSelect.element.classList.add("voice-select"); - win.speechSynthesis.addEventListener("voiceschanged", this); - dropdown.querySelector(".narrate-voices").appendChild( - this.voiceSelect.element); - - dropdown.addEventListener("click", this, true); - - let rateRange = dropdown.querySelector(".narrate-rate > input"); - rateRange.addEventListener("change", this); - - // The rate is stored as an integer. - rateRange.value = branch.getIntPref("rate"); - - this._setupVoices(); - - let tb = win.document.querySelector(".reader-toolbar"); - tb.appendChild(dropdown); -} - -NarrateControls.prototype = { - handleEvent(evt) { - switch (evt.type) { - case "change": - if (evt.target.classList.contains("narrate-rate-input")) { - this._onRateInput(evt); - } else { - this._onVoiceChange(); - } - break; - case "click": - this._onButtonClick(evt); - break; - case "voiceschanged": - this._setupVoices(); - break; - } - }, - - /** - * Returns true if synth voices are available. - */ - _setupVoices() { - return this._languagePromise.then(language => { - this.voiceSelect.clear(); - let win = this._win; - let voicePrefs = this._getVoicePref(); - let selectedVoice = voicePrefs[language || "default"]; - let comparer = win.Intl ? - (new Intl.Collator()).compare : (a, b) => a.localeCompare(b); - let filter = !Services.prefs.getBoolPref("narrate.filter-voices"); - let options = win.speechSynthesis.getVoices().filter(v => { - return filter || !language || v.lang.split("-")[0] == language; - }).map(v => { - return { - label: this._createVoiceLabel(v), - value: v.voiceURI, - selected: selectedVoice == v.voiceURI - }; - }).sort((a, b) => comparer(a.label, b.label)); - - if (options.length) { - options.unshift({ - label: gStrings.GetStringFromName("defaultvoice"), - value: "automatic", - selected: selectedVoice == "automatic" - }); - this.voiceSelect.addOptions(options); - } - - let narrateToggle = win.document.querySelector(".narrate-toggle"); - let initial = !this._voicesInitialized; - this._voicesInitialized = true; - - // We disable this entire feature if there are no available voices. - narrateToggle.hidden = !options.length; - }); - }, - - _getVoicePref() { - let voicePref = Services.prefs.getCharPref("narrate.voice"); - try { - return JSON.parse(voicePref); - } catch (e) { - return { default: voicePref }; - } - }, - - _onRateInput(evt) { - AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10)); - this.narrator.setRate(this._convertRate(evt.target.value)); - }, - - _onVoiceChange() { - let voice = this.voice; - this.narrator.setVoice(voice); - this._languagePromise.then(language => { - if (language) { - let voicePref = this._getVoicePref(); - voicePref[language || "default"] = voice; - AsyncPrefs.set("narrate.voice", JSON.stringify(voicePref)); - } - }); - }, - - _onButtonClick(evt) { - let classList = evt.target.classList; - if (classList.contains("narrate-skip-previous")) { - this.narrator.skipPrevious(); - } else if (classList.contains("narrate-skip-next")) { - this.narrator.skipNext(); - } else if (classList.contains("narrate-start-stop")) { - if (this.narrator.speaking) { - this.narrator.stop(); - } else { - this._updateSpeechControls(true); - let options = { rate: this.rate, voice: this.voice }; - this.narrator.start(options).then(() => { - this._updateSpeechControls(false); - }, err => { - Cu.reportError(`Narrate failed: ${err}.`); - this._updateSpeechControls(false); - }); - } - } - }, - - _updateSpeechControls(speaking) { - let dropdown = this._doc.querySelector(".narrate-dropdown"); - dropdown.classList.toggle("keep-open", speaking); - dropdown.classList.toggle("speaking", speaking); - - let startStopButton = this._doc.querySelector(".narrate-start-stop"); - startStopButton.title = - gStrings.GetStringFromName(speaking ? "stop" : "start"); - - this._doc.querySelector(".narrate-skip-previous").disabled = !speaking; - this._doc.querySelector(".narrate-skip-next").disabled = !speaking; - }, - - _createVoiceLabel(voice) { - // This is a highly imperfect method of making human-readable labels - // for system voices. Because each platform has a different naming scheme - // for voices, we use a different method for each platform. - switch (Services.appinfo.OS) { - case "WINNT": - // On windows the language is included in the name, so just use the name - return voice.name; - case "Linux": - // On Linux, the name is usually the unlocalized language name. - // Use a localized language name, and have the language tag in - // parenthisis. This is to avoid six languages called "English". - return gStrings.formatStringFromName("voiceLabel", - [this._getLanguageName(voice.lang) || voice.name, voice.lang], 2); - default: - // On Mac the language is not included in the name, find a localized - // language name or show the tag if none exists. - // This is the ideal naming scheme so it is also the "default". - return gStrings.formatStringFromName("voiceLabel", - [voice.name, this._getLanguageName(voice.lang) || voice.lang], 2); - } - }, - - _getLanguageName(lang) { - if (!this._langStrings) { - this._langStrings = Services.strings.createBundle( - "chrome://global/locale/languageNames.properties "); - } - - try { - // language tags will be lower case ascii between 2 and 3 characters long. - return this._langStrings.GetStringFromName(lang.match(/^[a-z]{2,3}/)[0]); - } catch (e) { - return ""; - } - }, - - _convertRate(rate) { - // We need to convert a relative percentage value to a fraction rate value. - // eg. -100 is half the speed, 100 is twice the speed in percentage, - // 0.5 is half the speed and 2 is twice the speed in fractions. - return Math.pow(Math.abs(rate / 100) + 1, rate < 0 ? -1 : 1); - }, - - get _win() { - return this._winRef.get(); - }, - - get _doc() { - return this._win.document; - }, - - get rate() { - return this._convertRate( - this._doc.querySelector(".narrate-rate-input").value); - }, - - get voice() { - return this.voiceSelect.value; - } -}; diff --git a/toolkit/components/narrate/Narrator.jsm b/toolkit/components/narrate/Narrator.jsm deleted file mode 100644 index ac0b2e040..000000000 --- a/toolkit/components/narrate/Narrator.jsm +++ /dev/null @@ -1,440 +0,0 @@ -/* 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"; - -const { interfaces: Ci, utils: Cu } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -this.EXPORTED_SYMBOLS = [ "Narrator" ]; - -// Maximum time into paragraph when pressing "skip previous" will go -// to previous paragraph and not the start of current one. -const PREV_THRESHOLD = 2000; -// All text-related style rules that we should copy over to the highlight node. -const kTextStylesRules = ["font-family", "font-kerning", "font-size", - "font-size-adjust", "font-stretch", "font-variant", "font-weight", - "line-height", "letter-spacing", "text-orientation", - "text-transform", "word-spacing"]; - -function Narrator(win, languagePromise) { - this._winRef = Cu.getWeakReference(win); - this._languagePromise = languagePromise; - this._inTest = Services.prefs.getBoolPref("narrate.test"); - this._speechOptions = {}; - this._startTime = 0; - this._stopped = false; -} - -Narrator.prototype = { - get _doc() { - return this._winRef.get().document; - }, - - get _win() { - return this._winRef.get(); - }, - - get _treeWalker() { - if (!this._treeWalkerRef) { - let wu = this._win.QueryInterface( - Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); - let nf = this._win.NodeFilter; - - let filter = { - _matches: new Set(), - - // We want high-level elements that have non-empty text nodes. - // For example, paragraphs. But nested anchors and other elements - // are not interesting since their text already appears in their - // parent's textContent. - acceptNode(node) { - if (this._matches.has(node.parentNode)) { - // Reject sub-trees of accepted nodes. - return nf.FILTER_REJECT; - } - - if (!/\S/.test(node.textContent)) { - // Reject nodes with no text. - return nf.FILTER_REJECT; - } - - let bb = wu.getBoundsWithoutFlushing(node); - if (!bb.width || !bb.height) { - // Skip non-rendered nodes. We don't reject because a zero-sized - // container can still have visible, "overflowed", content. - return nf.FILTER_SKIP; - } - - for (let c = node.firstChild; c; c = c.nextSibling) { - if (c.nodeType == c.TEXT_NODE && /\S/.test(c.textContent)) { - // If node has a non-empty text child accept it. - this._matches.add(node); - return nf.FILTER_ACCEPT; - } - } - - return nf.FILTER_SKIP; - } - }; - - this._treeWalkerRef = new WeakMap(); - - // We can't hold a weak reference on the treewalker, because there - // are no other strong references, and it will be GC'ed. Instead, - // we rely on the window's lifetime and use it as a weak reference. - this._treeWalkerRef.set(this._win, - this._doc.createTreeWalker(this._doc.querySelector(".container"), - nf.SHOW_ELEMENT, filter, false)); - } - - return this._treeWalkerRef.get(this._win); - }, - - get _timeIntoParagraph() { - let rv = Date.now() - this._startTime; - return rv; - }, - - get speaking() { - return this._win.speechSynthesis.speaking || - this._win.speechSynthesis.pending; - }, - - _getVoice(voiceURI) { - if (!this._voiceMap || !this._voiceMap.has(voiceURI)) { - this._voiceMap = new Map( - this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v])); - } - - return this._voiceMap.get(voiceURI); - }, - - _isParagraphInView(paragraph) { - if (!paragraph) { - return false; - } - - let bb = paragraph.getBoundingClientRect(); - return bb.top >= 0 && bb.top < this._win.innerHeight; - }, - - _sendTestEvent(eventType, detail) { - let win = this._win; - win.dispatchEvent(new win.CustomEvent(eventType, - { detail: Cu.cloneInto(detail, win.document) })); - }, - - _speakInner() { - this._win.speechSynthesis.cancel(); - let tw = this._treeWalker; - let paragraph = tw.currentNode; - if (paragraph == tw.root) { - this._sendTestEvent("paragraphsdone", {}); - return Promise.resolve(); - } - - let utterance = new this._win.SpeechSynthesisUtterance( - paragraph.textContent); - utterance.rate = this._speechOptions.rate; - if (this._speechOptions.voice) { - utterance.voice = this._speechOptions.voice; - } else { - utterance.lang = this._speechOptions.lang; - } - - this._startTime = Date.now(); - - let highlighter = new Highlighter(paragraph); - - if (this._inTest) { - let onTestSynthEvent = e => { - if (e.detail.type == "boundary") { - let args = Object.assign({ utterance }, e.detail.args); - let evt = new this._win.SpeechSynthesisEvent(e.detail.type, args); - utterance.dispatchEvent(evt); - } - }; - - let removeListeners = () => { - this._win.removeEventListener("testsynthevent", onTestSynthEvent); - }; - - this._win.addEventListener("testsynthevent", onTestSynthEvent); - utterance.addEventListener("end", removeListeners); - utterance.addEventListener("error", removeListeners); - } - - return new Promise((resolve, reject) => { - utterance.addEventListener("start", () => { - paragraph.classList.add("narrating"); - let bb = paragraph.getBoundingClientRect(); - if (bb.top < 0 || bb.bottom > this._win.innerHeight) { - paragraph.scrollIntoView({ behavior: "smooth", block: "start"}); - } - - if (this._inTest) { - this._sendTestEvent("paragraphstart", { - voice: utterance.chosenVoiceURI, - rate: utterance.rate, - paragraph: paragraph.textContent, - tag: paragraph.localName - }); - } - }); - - utterance.addEventListener("end", () => { - if (!this._win) { - // page got unloaded, don't do anything. - return; - } - - highlighter.remove(); - paragraph.classList.remove("narrating"); - this._startTime = 0; - if (this._inTest) { - this._sendTestEvent("paragraphend", {}); - } - - if (this._stopped) { - // User pressed stopped. - resolve(); - } else { - tw.currentNode = tw.nextNode() || tw.root; - this._speakInner().then(resolve, reject); - } - }); - - utterance.addEventListener("error", () => { - reject("speech synthesis failed"); - }); - - utterance.addEventListener("boundary", e => { - if (e.name != "word") { - // We are only interested in word boundaries for now. - return; - } - - if (e.charLength) { - highlighter.highlight(e.charIndex, e.charLength); - if (this._inTest) { - this._sendTestEvent("wordhighlight", { - start: e.charIndex, - end: e.charIndex + e.charLength - }); - } - } - }); - - this._win.speechSynthesis.speak(utterance); - }); - }, - - start(speechOptions) { - this._speechOptions = { - rate: speechOptions.rate, - voice: this._getVoice(speechOptions.voice) - }; - - this._stopped = false; - return this._languagePromise.then(language => { - if (!this._speechOptions.voice) { - this._speechOptions.lang = language; - } - - let tw = this._treeWalker; - if (!this._isParagraphInView(tw.currentNode)) { - tw.currentNode = tw.root; - while (tw.nextNode()) { - if (this._isParagraphInView(tw.currentNode)) { - break; - } - } - } - if (tw.currentNode == tw.root) { - tw.nextNode(); - } - - return this._speakInner(); - }); - }, - - stop() { - this._stopped = true; - this._win.speechSynthesis.cancel(); - }, - - skipNext() { - this._win.speechSynthesis.cancel(); - }, - - skipPrevious() { - this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1); - }, - - setRate(rate) { - this._speechOptions.rate = rate; - /* repeat current paragraph */ - this._goBackParagraphs(1); - }, - - setVoice(voice) { - this._speechOptions.voice = this._getVoice(voice); - /* repeat current paragraph */ - this._goBackParagraphs(1); - }, - - _goBackParagraphs(count) { - let tw = this._treeWalker; - for (let i = 0; i < count; i++) { - if (!tw.previousNode()) { - tw.currentNode = tw.root; - } - } - this._win.speechSynthesis.cancel(); - } -}; - -/** - * The Highlighter class is used to highlight a range of text in a container. - * - * @param {nsIDOMElement} container a text container - */ -function Highlighter(container) { - this.container = container; -} - -Highlighter.prototype = { - /** - * Highlight the range within offsets relative to the container. - * - * @param {Number} startOffset the start offset - * @param {Number} length the length in characters of the range - */ - highlight(startOffset, length) { - let containerRect = this.container.getBoundingClientRect(); - let range = this._getRange(startOffset, startOffset + length); - let rangeRects = range.getClientRects(); - let win = this.container.ownerGlobal; - let computedStyle = win.getComputedStyle(range.endContainer.parentNode); - let nodes = this._getFreshHighlightNodes(rangeRects.length); - - let textStyle = {}; - for (let textStyleRule of kTextStylesRules) { - textStyle[textStyleRule] = computedStyle[textStyleRule]; - } - - for (let i = 0; i < rangeRects.length; i++) { - let r = rangeRects[i]; - let node = nodes[i]; - - let style = Object.assign({ - "top": `${r.top - containerRect.top + r.height / 2}px`, - "left": `${r.left - containerRect.left + r.width / 2}px`, - "width": `${r.width}px`, - "height": `${r.height}px` - }, textStyle); - - // Enables us to vary the CSS transition on a line change. - node.classList.toggle("newline", style.top != node.dataset.top); - node.dataset.top = style.top; - - // Enables CSS animations. - node.classList.remove("animate"); - win.requestAnimationFrame(() => { - node.classList.add("animate"); - }); - - // Enables alternative word display with a CSS pseudo-element. - node.dataset.word = range.toString(); - - // Apply style - node.style = Object.entries(style).map( - s => `${s[0]}: ${s[1]};`).join(" "); - } - }, - - /** - * Releases reference to container and removes all highlight nodes. - */ - remove() { - for (let node of this._nodes) { - node.remove(); - } - - this.container = null; - }, - - /** - * Returns specified amount of highlight nodes. Creates new ones if necessary - * and purges any additional nodes that are not needed. - * - * @param {Number} count number of nodes needed - */ - _getFreshHighlightNodes(count) { - let doc = this.container.ownerDocument; - let nodes = Array.from(this._nodes); - - // Remove nodes we don't need anymore (nodes.length - count > 0). - for (let toRemove = 0; toRemove < nodes.length - count; toRemove++) { - nodes.shift().remove(); - } - - // Add additional nodes if we need them (count - nodes.length > 0). - for (let toAdd = 0; toAdd < count - nodes.length; toAdd++) { - let node = doc.createElement("div"); - node.className = "narrate-word-highlight"; - this.container.appendChild(node); - nodes.push(node); - } - - return nodes; - }, - - /** - * Create and return a range object with the start and end offsets relative - * to the container node. - * - * @param {Number} startOffset the start offset - * @param {Number} endOffset the end offset - */ - _getRange(startOffset, endOffset) { - let doc = this.container.ownerDocument; - let i = 0; - let treeWalker = doc.createTreeWalker( - this.container, doc.defaultView.NodeFilter.SHOW_TEXT); - let node = treeWalker.nextNode(); - - function _findNodeAndOffset(offset) { - do { - let length = node.data.length; - if (offset >= i && offset <= i + length) { - return [node, offset - i]; - } - i += length; - } while ((node = treeWalker.nextNode())); - - // Offset is out of bounds, return last offset of last node. - node = treeWalker.lastChild(); - return [node, node.data.length]; - } - - let range = doc.createRange(); - range.setStart(..._findNodeAndOffset(startOffset)); - range.setEnd(..._findNodeAndOffset(endOffset)); - - return range; - }, - - /* - * Get all existing highlight nodes for container. - */ - get _nodes() { - return this.container.querySelectorAll(".narrate-word-highlight"); - } -}; diff --git a/toolkit/components/narrate/VoiceSelect.jsm b/toolkit/components/narrate/VoiceSelect.jsm deleted file mode 100644 index 861a21c97..000000000 --- a/toolkit/components/narrate/VoiceSelect.jsm +++ /dev/null @@ -1,300 +0,0 @@ -/* 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"; - -const Cu = Components.utils; - -this.EXPORTED_SYMBOLS = ["VoiceSelect"]; - -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("keypress", this); - - let listbox = this.listbox; - listbox.addEventListener("click", this); - listbox.addEventListener("mousemove", this); - listbox.addEventListener("keypress", 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": - 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 "keypress": - if (target.classList.contains("select-toggle")) { - if (evt.altKey) { - this.toggleList(true); - } else { - this._keyPressedButton(evt); - } - } else { - this.listbox.classList.remove("hovering"); - this._keyPressedInBox(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; - }, - - _keyPressedButton(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; - } - }, - - _keyPressedInBox(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 : ""; - } -}; diff --git a/toolkit/components/narrate/moz.build b/toolkit/components/narrate/moz.build deleted file mode 100644 index fe1dfb6ca..000000000 --- a/toolkit/components/narrate/moz.build +++ /dev/null @@ -1,10 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# 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/. - -EXTRA_JS_MODULES.narrate = [ - 'NarrateControls.jsm', - 'Narrator.jsm', - 'VoiceSelect.jsm' -] |