diff options
author | Moonchild <mcwerewolf@gmail.com> | 2018-05-16 17:10:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-16 17:10:38 +0200 |
commit | 90942a2af0cabb9345cf04fa6113e12197504fcf (patch) | |
tree | e16c71be5a1343abe0489863f84ed271b6ebd3d7 /toolkit/components/narrate | |
parent | 819ca50f163a9113772a7dbfd617d97151893337 (diff) | |
parent | 9ef464a5ac0a17135a0f7b4fef070bb4f7fbe44c (diff) | |
download | UXP-90942a2af0cabb9345cf04fa6113e12197504fcf.tar UXP-90942a2af0cabb9345cf04fa6113e12197504fcf.tar.gz UXP-90942a2af0cabb9345cf04fa6113e12197504fcf.tar.lz UXP-90942a2af0cabb9345cf04fa6113e12197504fcf.tar.xz UXP-90942a2af0cabb9345cf04fa6113e12197504fcf.zip |
Merge pull request #367 from Ascrod/readerview
Reader and Narrator Updates
Diffstat (limited to 'toolkit/components/narrate')
-rw-r--r-- | toolkit/components/narrate/NarrateControls.jsm | 135 | ||||
-rw-r--r-- | toolkit/components/narrate/Narrator.jsm | 78 | ||||
-rw-r--r-- | toolkit/components/narrate/VoiceSelect.jsm | 23 |
3 files changed, 103 insertions, 133 deletions
diff --git a/toolkit/components/narrate/NarrateControls.jsm b/toolkit/components/narrate/NarrateControls.jsm index 7d8794b18..be3ce636c 100644 --- a/toolkit/components/narrate/NarrateControls.jsm +++ b/toolkit/components/narrate/NarrateControls.jsm @@ -16,9 +16,9 @@ this.EXPORTED_SYMBOLS = ["NarrateControls"]; var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties"); -function NarrateControls(mm, win) { - this._mm = mm; +function NarrateControls(win, languagePromise) { this._winRef = Cu.getWeakReference(win); + this._languagePromise = languagePromise; win.addEventListener("unload", this); @@ -37,16 +37,12 @@ function NarrateControls(mm, win) { } let dropdown = win.document.createElement("ul"); - dropdown.className = "dropdown"; - dropdown.id = "narrate-dropdown"; + dropdown.className = "dropdown narrate-dropdown"; // We need inline svg here for the animation to work (bug 908634 & 1190881). - // The style animation can't be scoped (bug 830056). + // eslint-disable-next-line no-unsanitized/property dropdown.innerHTML = - localize`<style scoped> - @import url("chrome://global/skin/narrateControls.css"); - </style> - <li> - <button class="dropdown-toggle button" id="narrate-toggle" + 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" @@ -60,11 +56,11 @@ function NarrateControls(mm, win) { 100% { transform: scaleY(1); } } - #waveform > rect { + .waveform > rect { fill: #808080; } - .speaking #waveform > rect { + .speaking .waveform > rect { fill: #58bf43; transform-box: fill-box; transform-origin: 50% 50%; @@ -74,15 +70,15 @@ function NarrateControls(mm, win) { 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; } + .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 id="waveform"> + <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" /> @@ -95,35 +91,35 @@ function NarrateControls(mm, win) { </button> </li> <li class="dropdown-popup"> - <div id="narrate-control" class="narrate-row"> - <button disabled id="narrate-skip-previous" + <div class="narrate-row narrate-control"> + <button disabled class="narrate-skip-previous" title="${"back"}"></button> - <button id="narrate-start-stop" title="${"start"}"></button> - <button disabled id="narrate-skip-next" + <button class="narrate-start-stop" title="${"start"}"></button> + <button disabled class="narrate-skip-next" title="${"forward"}"></button> </div> - <div id="narrate-rate" class="narrate-row"> - <input id="narrate-rate-input" value="0" title="${"speed"}" + <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 id="narrate-voices" class="narrate-row"></div> + <div class="narrate-row narrate-voices"></div> <div class="dropdown-arrow"></div> </li>`; - this.narrator = new Narrator(win); + 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.id = "voice-select"; + this.voiceSelect.element.classList.add("voice-select"); win.speechSynthesis.addEventListener("voiceschanged", this); - dropdown.querySelector("#narrate-voices").appendChild( + dropdown.querySelector(".narrate-voices").appendChild( this.voiceSelect.element); dropdown.addEventListener("click", this, true); - let rateRange = dropdown.querySelector("#narrate-rate > input"); + let rateRange = dropdown.querySelector(".narrate-rate > input"); rateRange.addEventListener("change", this); // The rate is stored as an integer. @@ -131,15 +127,15 @@ function NarrateControls(mm, win) { this._setupVoices(); - let tb = win.document.getElementById("reader-toolbar"); + let tb = win.document.querySelector(".reader-toolbar"); tb.appendChild(dropdown); } NarrateControls.prototype = { - handleEvent: function(evt) { + handleEvent(evt) { switch (evt.type) { case "change": - if (evt.target.id == "narrate-rate-input") { + if (evt.target.classList.contains("narrate-rate-input")) { this._onRateInput(evt); } else { this._onVoiceChange(); @@ -162,8 +158,8 @@ NarrateControls.prototype = { /** * Returns true if synth voices are available. */ - _setupVoices: function() { - return this.narrator.languagePromise.then(language => { + _setupVoices() { + return this._languagePromise.then(language => { this.voiceSelect.clear(); let win = this._win; let voicePrefs = this._getVoicePref(); @@ -190,7 +186,7 @@ NarrateControls.prototype = { this.voiceSelect.addOptions(options); } - let narrateToggle = win.document.getElementById("narrate-toggle"); + let narrateToggle = win.document.querySelector(".narrate-toggle"); let histogram = Services.telemetry.getKeyedHistogramById( "NARRATE_CONTENT_BY_LANGUAGE_2"); let initial = !this._voicesInitialized; @@ -210,7 +206,7 @@ NarrateControls.prototype = { }); }, - _getVoicePref: function() { + _getVoicePref() { let voicePref = Services.prefs.getCharPref("narrate.voice"); try { return JSON.parse(voicePref); @@ -219,15 +215,15 @@ NarrateControls.prototype = { } }, - _onRateInput: function(evt) { + _onRateInput(evt) { AsyncPrefs.set("narrate.rate", parseInt(evt.target.value, 10)); this.narrator.setRate(this._convertRate(evt.target.value)); }, - _onVoiceChange: function() { + _onVoiceChange() { let voice = this.voice; this.narrator.setVoice(voice); - this.narrator.languagePromise.then(language => { + this._languagePromise.then(language => { if (language) { let voicePref = this._getVoicePref(); voicePref[language || "default"] = voice; @@ -236,42 +232,39 @@ NarrateControls.prototype = { }); }, - _onButtonClick: function(evt) { - switch (evt.target.id) { - case "narrate-skip-previous": - this.narrator.skipPrevious(); - break; - case "narrate-skip-next": - this.narrator.skipNext(); - break; - case "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); - }); - } - break; + _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: function(speaking) { - let dropdown = this._doc.getElementById("narrate-dropdown"); + _updateSpeechControls(speaking) { + let dropdown = this._doc.querySelector(".narrate-dropdown"); dropdown.classList.toggle("keep-open", speaking); dropdown.classList.toggle("speaking", speaking); - let startStopButton = this._doc.getElementById("narrate-start-stop"); + let startStopButton = this._doc.querySelector(".narrate-start-stop"); startStopButton.title = gStrings.GetStringFromName(speaking ? "stop" : "start"); - this._doc.getElementById("narrate-skip-previous").disabled = !speaking; - this._doc.getElementById("narrate-skip-next").disabled = !speaking; + this._doc.querySelector(".narrate-skip-previous").disabled = !speaking; + this._doc.querySelector(".narrate-skip-next").disabled = !speaking; if (speaking) { TelemetryStopwatch.start("NARRATE_CONTENT_SPEAKTIME_MS", this); @@ -280,7 +273,7 @@ NarrateControls.prototype = { } }, - _createVoiceLabel: function(voice) { + _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. @@ -303,7 +296,7 @@ NarrateControls.prototype = { } }, - _getLanguageName: function(lang) { + _getLanguageName(lang) { if (!this._langStrings) { this._langStrings = Services.strings.createBundle( "chrome://global/locale/languageNames.properties "); @@ -317,7 +310,7 @@ NarrateControls.prototype = { } }, - _convertRate: function(rate) { + _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. @@ -334,7 +327,7 @@ NarrateControls.prototype = { get rate() { return this._convertRate( - this._doc.getElementById("narrate-rate-input").value); + this._doc.querySelector(".narrate-rate-input").value); }, get voice() { diff --git a/toolkit/components/narrate/Narrator.jsm b/toolkit/components/narrate/Narrator.jsm index ade06510e..ac0b2e040 100644 --- a/toolkit/components/narrate/Narrator.jsm +++ b/toolkit/components/narrate/Narrator.jsm @@ -8,8 +8,6 @@ const { interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector", - "resource:///modules/translation/LanguageDetector.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); @@ -24,29 +22,13 @@ const kTextStylesRules = ["font-family", "font-kerning", "font-size", "line-height", "letter-spacing", "text-orientation", "text-transform", "word-spacing"]; -function Narrator(win) { +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; - - this.languagePromise = new Promise(resolve => { - let detect = () => { - win.document.removeEventListener("AboutReaderContentReady", detect); - let sampleText = this._doc.getElementById( - "moz-reader-content").textContent.substring(0, 60 * 1024); - LanguageDetector.detectLanguage(sampleText).then(result => { - resolve(result.confident ? result.language : null); - }); - }; - - if (win.document.body.classList.contains("loaded")) { - detect(); - } else { - win.document.addEventListener("AboutReaderContentReady", detect); - } - }); } Narrator.prototype = { @@ -71,7 +53,7 @@ Narrator.prototype = { // For example, paragraphs. But nested anchors and other elements // are not interesting since their text already appears in their // parent's textContent. - acceptNode: function(node) { + acceptNode(node) { if (this._matches.has(node.parentNode)) { // Reject sub-trees of accepted nodes. return nf.FILTER_REJECT; @@ -107,7 +89,7 @@ Narrator.prototype = { // 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.getElementById("container"), + this._doc.createTreeWalker(this._doc.querySelector(".container"), nf.SHOW_ELEMENT, filter, false)); } @@ -124,7 +106,7 @@ Narrator.prototype = { this._win.speechSynthesis.pending; }, - _getVoice: function(voiceURI) { + _getVoice(voiceURI) { if (!this._voiceMap || !this._voiceMap.has(voiceURI)) { this._voiceMap = new Map( this._win.speechSynthesis.getVoices().map(v => [v.voiceURI, v])); @@ -133,7 +115,7 @@ Narrator.prototype = { return this._voiceMap.get(voiceURI); }, - _isParagraphInView: function(paragraph) { + _isParagraphInView(paragraph) { if (!paragraph) { return false; } @@ -142,13 +124,13 @@ Narrator.prototype = { return bb.top >= 0 && bb.top < this._win.innerHeight; }, - _sendTestEvent: function(eventType, detail) { + _sendTestEvent(eventType, detail) { let win = this._win; win.dispatchEvent(new win.CustomEvent(eventType, { detail: Cu.cloneInto(detail, win.document) })); }, - _speakInner: function() { + _speakInner() { this._win.speechSynthesis.cancel(); let tw = this._treeWalker; let paragraph = tw.currentNode; @@ -238,18 +220,12 @@ Narrator.prototype = { return; } - // Match non-whitespace. This isn't perfect, but the most universal - // solution for now. - let reWordBoundary = /\S+/g; - // Match the first word from the boundary event offset. - reWordBoundary.lastIndex = e.charIndex; - let firstIndex = reWordBoundary.exec(paragraph.textContent); - if (firstIndex) { - highlighter.highlight(firstIndex.index, reWordBoundary.lastIndex); + if (e.charLength) { + highlighter.highlight(e.charIndex, e.charLength); if (this._inTest) { this._sendTestEvent("wordhighlight", { - start: firstIndex.index, - end: reWordBoundary.lastIndex + start: e.charIndex, + end: e.charIndex + e.charLength }); } } @@ -259,14 +235,14 @@ Narrator.prototype = { }); }, - start: function(speechOptions) { + start(speechOptions) { this._speechOptions = { rate: speechOptions.rate, voice: this._getVoice(speechOptions.voice) }; this._stopped = false; - return this.languagePromise.then(language => { + return this._languagePromise.then(language => { if (!this._speechOptions.voice) { this._speechOptions.lang = language; } @@ -288,32 +264,32 @@ Narrator.prototype = { }); }, - stop: function() { + stop() { this._stopped = true; this._win.speechSynthesis.cancel(); }, - skipNext: function() { + skipNext() { this._win.speechSynthesis.cancel(); }, - skipPrevious: function() { + skipPrevious() { this._goBackParagraphs(this._timeIntoParagraph < PREV_THRESHOLD ? 2 : 1); }, - setRate: function(rate) { + setRate(rate) { this._speechOptions.rate = rate; /* repeat current paragraph */ this._goBackParagraphs(1); }, - setVoice: function(voice) { + setVoice(voice) { this._speechOptions.voice = this._getVoice(voice); /* repeat current paragraph */ this._goBackParagraphs(1); }, - _goBackParagraphs: function(count) { + _goBackParagraphs(count) { let tw = this._treeWalker; for (let i = 0; i < count; i++) { if (!tw.previousNode()) { @@ -338,13 +314,13 @@ Highlighter.prototype = { * Highlight the range within offsets relative to the container. * * @param {Number} startOffset the start offset - * @param {Number} endOffset the end offset + * @param {Number} length the length in characters of the range */ - highlight: function(startOffset, endOffset) { + highlight(startOffset, length) { let containerRect = this.container.getBoundingClientRect(); - let range = this._getRange(startOffset, endOffset); + let range = this._getRange(startOffset, startOffset + length); let rangeRects = range.getClientRects(); - let win = this.container.ownerDocument.defaultView; + let win = this.container.ownerGlobal; let computedStyle = win.getComputedStyle(range.endContainer.parentNode); let nodes = this._getFreshHighlightNodes(rangeRects.length); @@ -386,7 +362,7 @@ Highlighter.prototype = { /** * Releases reference to container and removes all highlight nodes. */ - remove: function() { + remove() { for (let node of this._nodes) { node.remove(); } @@ -400,7 +376,7 @@ Highlighter.prototype = { * * @param {Number} count number of nodes needed */ - _getFreshHighlightNodes: function(count) { + _getFreshHighlightNodes(count) { let doc = this.container.ownerDocument; let nodes = Array.from(this._nodes); @@ -427,7 +403,7 @@ Highlighter.prototype = { * @param {Number} startOffset the start offset * @param {Number} endOffset the end offset */ - _getRange: function(startOffset, endOffset) { + _getRange(startOffset, endOffset) { let doc = this.container.ownerDocument; let i = 0; let treeWalker = doc.createTreeWalker( diff --git a/toolkit/components/narrate/VoiceSelect.jsm b/toolkit/components/narrate/VoiceSelect.jsm index b283a06b3..861a21c97 100644 --- a/toolkit/components/narrate/VoiceSelect.jsm +++ b/toolkit/components/narrate/VoiceSelect.jsm @@ -13,6 +13,7 @@ function VoiceSelect(win, label) { 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> @@ -37,7 +38,7 @@ function VoiceSelect(win, label) { } VoiceSelect.prototype = { - add: function(label, value) { + add(label, value) { let option = this._doc.createElement("button"); option.dataset.value = value; option.classList.add("option"); @@ -48,7 +49,7 @@ VoiceSelect.prototype = { return option; }, - addOptions: function(options) { + addOptions(options) { let selected = null; for (let option of options) { if (option.selected) { @@ -61,11 +62,11 @@ VoiceSelect.prototype = { this._select(selected || this.options[0], true); }, - clear: function() { + clear() { this.listbox.innerHTML = ""; }, - toggleList: function(force, focus = true) { + toggleList(force, focus = true) { if (this.element.classList.toggle("open", force)) { if (focus) { (this.selected || this.options[0]).focus(); @@ -84,7 +85,7 @@ VoiceSelect.prototype = { } }, - handleEvent: function(evt) { + handleEvent(evt) { let target = evt.target; switch (evt.type) { @@ -131,7 +132,7 @@ VoiceSelect.prototype = { } }, - _getPagedOption: function(option, up) { + _getPagedOption(option, up) { let height = elem => elem.getBoundingClientRect().height; let listboxHeight = height(this.listbox); @@ -148,7 +149,7 @@ VoiceSelect.prototype = { return next; }, - _keyPressedButton: function(evt) { + _keyPressedButton(evt) { if (evt.altKey && (evt.key === "ArrowUp" || evt.key === "ArrowUp")) { this.toggleList(true); return; @@ -178,7 +179,7 @@ VoiceSelect.prototype = { } }, - _keyPressedInBox: function(evt) { + _keyPressedInBox(evt) { let toFocus; let cur = this._doc.activeElement; @@ -212,7 +213,7 @@ VoiceSelect.prototype = { } }, - _select: function(option, suppressEvent = false) { + _select(option, suppressEvent = false) { let oldSelected = this.selected; if (oldSelected) { oldSelected.removeAttribute("aria-selected"); @@ -233,7 +234,7 @@ VoiceSelect.prototype = { } }, - _updateDropdownHeight: function(now) { + _updateDropdownHeight(now) { let updateInner = () => { let winHeight = this._win.innerHeight; let listbox = this.listbox; @@ -252,7 +253,7 @@ VoiceSelect.prototype = { } }, - _getOptionFromValue: function(value) { + _getOptionFromValue(value) { return Array.from(this.options).find(o => o.dataset.value === value); }, |