summaryrefslogtreecommitdiffstats
path: root/toolkit/components/narrate
diff options
context:
space:
mode:
authorMoonchild <mcwerewolf@gmail.com>2018-05-16 17:10:38 +0200
committerGitHub <noreply@github.com>2018-05-16 17:10:38 +0200
commit90942a2af0cabb9345cf04fa6113e12197504fcf (patch)
treee16c71be5a1343abe0489863f84ed271b6ebd3d7 /toolkit/components/narrate
parent819ca50f163a9113772a7dbfd617d97151893337 (diff)
parent9ef464a5ac0a17135a0f7b4fef070bb4f7fbe44c (diff)
downloadUXP-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.jsm135
-rw-r--r--toolkit/components/narrate/Narrator.jsm78
-rw-r--r--toolkit/components/narrate/VoiceSelect.jsm23
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);
},