summaryrefslogtreecommitdiff
path: root/toolkit/components/narrate
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2021-11-14 01:30:12 -0500
committerMatt A. Tobin <email@mattatobin.com>2021-11-14 01:30:12 -0500
commit43fa0a71c814e902165782680f2f9705e3c234c9 (patch)
tree0ab3c66e8432febc00c18c036706c7ff43c11799 /toolkit/components/narrate
parent2fccdffac3075ee72087f9ee153204b02c7d931a (diff)
downloadaura-central-43fa0a71c814e902165782680f2f9705e3c234c9.tar.gz
Issue %3005 - Move toolkit/components to components/
Diffstat (limited to 'toolkit/components/narrate')
-rw-r--r--toolkit/components/narrate/.eslintrc.js94
-rw-r--r--toolkit/components/narrate/NarrateControls.jsm313
-rw-r--r--toolkit/components/narrate/Narrator.jsm440
-rw-r--r--toolkit/components/narrate/VoiceSelect.jsm300
-rw-r--r--toolkit/components/narrate/moz.build10
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'
-]