From b70d884598e1e14b99190e1e5c349553ff59849b Mon Sep 17 00:00:00 2001 From: Ascrod <32915892+Ascrod@users.noreply.github.com> Date: Mon, 7 May 2018 19:45:46 -0400 Subject: Initial updates for Reader View. --- toolkit/components/narrate/NarrateControls.jsm | 134 ++-- toolkit/components/narrate/Narrator.jsm | 78 +- toolkit/components/narrate/VoiceSelect.jsm | 23 +- .../components/printing/content/simplifyMode.css | 2 +- toolkit/components/reader/AboutReader.jsm | 314 ++++---- toolkit/components/reader/JSDOMParser.js | 46 +- toolkit/components/reader/Readability.js | 853 +++++++++------------ toolkit/components/reader/ReaderMode.jsm | 312 +++++--- toolkit/components/reader/ReaderWorker.js | 7 +- toolkit/components/reader/content/aboutReader.html | 73 +- toolkit/components/reader/content/aboutReader.js | 2 +- toolkit/content/browser-content.js | 6 - .../en-US/chrome/global/aboutReader.properties | 16 + toolkit/themes/shared/aboutReader.css | 626 ++++++++++++++- toolkit/themes/shared/aboutReaderContent.css | 177 ----- toolkit/themes/shared/aboutReaderControls.css | 388 ---------- toolkit/themes/shared/jar.inc.mn | 5 +- toolkit/themes/shared/narrate.css | 195 ++++- toolkit/themes/shared/narrateControls.css | 185 ----- .../shared/reader/RM-Content-Width-Minus-42x16.svg | 5 +- .../shared/reader/RM-Content-Width-Plus-44x16.svg | 5 +- .../shared/reader/RM-Line-Height-Minus-38x14.svg | 5 +- .../shared/reader/RM-Line-Height-Plus-38x24.svg | 5 +- .../shared/reader/RM-Type-Controls-Arrow.svg | 16 +- 24 files changed, 1742 insertions(+), 1736 deletions(-) delete mode 100644 toolkit/themes/shared/aboutReaderContent.css delete mode 100644 toolkit/themes/shared/aboutReaderControls.css delete mode 100644 toolkit/themes/shared/narrateControls.css (limited to 'toolkit') diff --git a/toolkit/components/narrate/NarrateControls.jsm b/toolkit/components/narrate/NarrateControls.jsm index 7d8794b18..828292cc2 100644 --- a/toolkit/components/narrate/NarrateControls.jsm +++ b/toolkit/components/narrate/NarrateControls.jsm @@ -16,9 +16,10 @@ this.EXPORTED_SYMBOLS = ["NarrateControls"]; var gStrings = Services.strings.createBundle("chrome://global/locale/narrate.properties"); -function NarrateControls(mm, win) { +function NarrateControls(mm, win, languagePromise) { this._mm = mm; this._winRef = Cu.getWeakReference(win); + this._languagePromise = languagePromise; win.addEventListener("unload", this); @@ -37,16 +38,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` -
  • -
  • `; - 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 +128,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 +159,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 +187,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 +207,7 @@ NarrateControls.prototype = { }); }, - _getVoicePref: function() { + _getVoicePref() { let voicePref = Services.prefs.getCharPref("narrate.voice"); try { return JSON.parse(voicePref); @@ -219,15 +216,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 +233,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 +274,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 +297,7 @@ NarrateControls.prototype = { } }, - _getLanguageName: function(lang) { + _getLanguageName(lang) { if (!this._langStrings) { this._langStrings = Services.strings.createBundle( "chrome://global/locale/languageNames.properties "); @@ -317,7 +311,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 +328,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 = `