forked from mirrors/gecko-dev
1557 lines
43 KiB
JavaScript
1557 lines
43 KiB
JavaScript
/* 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/. */
|
||
|
||
const { ReaderMode } = ChromeUtils.import(
|
||
"resource://gre/modules/ReaderMode.jsm"
|
||
);
|
||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||
|
||
const lazy = {};
|
||
|
||
ChromeUtils.defineESModuleGetters(lazy, {
|
||
AsyncPrefs: "resource://gre/modules/AsyncPrefs.sys.mjs",
|
||
});
|
||
ChromeUtils.defineModuleGetter(
|
||
lazy,
|
||
"NarrateControls",
|
||
"resource://gre/modules/narrate/NarrateControls.jsm"
|
||
);
|
||
ChromeUtils.defineModuleGetter(
|
||
lazy,
|
||
"NimbusFeatures",
|
||
"resource://nimbus/ExperimentAPI.jsm"
|
||
);
|
||
|
||
XPCOMUtils.defineLazyGetter(
|
||
lazy,
|
||
"numberFormat",
|
||
() => new Services.intl.NumberFormat(undefined)
|
||
);
|
||
XPCOMUtils.defineLazyGetter(
|
||
lazy,
|
||
"pluralRules",
|
||
() => new Services.intl.PluralRules(undefined)
|
||
);
|
||
|
||
const COLORSCHEME_L10N_IDS = {
|
||
light: "about-reader-color-scheme-light",
|
||
dark: "about-reader-color-scheme-dark",
|
||
sepia: "about-reader-color-scheme-sepia",
|
||
auto: "about-reader-color-scheme-auto",
|
||
};
|
||
|
||
Services.telemetry.setEventRecordingEnabled("readermode", true);
|
||
|
||
const zoomOnCtrl =
|
||
Services.prefs.getIntPref("mousewheel.with_control.action", 3) == 3;
|
||
const zoomOnMeta =
|
||
Services.prefs.getIntPref("mousewheel.with_meta.action", 1) == 3;
|
||
const isAppLocaleRTL = Services.locale.isAppLocaleRTL;
|
||
|
||
export var AboutReader = function(
|
||
actor,
|
||
articlePromise,
|
||
docContentType = "document",
|
||
docTitle = ""
|
||
) {
|
||
let win = actor.contentWindow;
|
||
let url = this._getOriginalUrl(win);
|
||
if (
|
||
!(
|
||
url.startsWith("http://") ||
|
||
url.startsWith("https://") ||
|
||
url.startsWith("file://")
|
||
)
|
||
) {
|
||
let errorMsg =
|
||
"Only http://, https:// and file:// URLs can be loaded in about:reader.";
|
||
if (Services.prefs.getBoolPref("reader.errors.includeURLs")) {
|
||
errorMsg += " Tried to load: " + url + ".";
|
||
}
|
||
console.error(errorMsg);
|
||
win.location.href = "about:blank";
|
||
return;
|
||
}
|
||
|
||
let doc = win.document;
|
||
if (isAppLocaleRTL) {
|
||
doc.dir = "rtl";
|
||
}
|
||
doc.documentElement.setAttribute("platform", AppConstants.platform);
|
||
|
||
doc.title = docTitle;
|
||
|
||
this._actor = actor;
|
||
this._isLoggedInPocketUser = undefined;
|
||
|
||
this._docRef = Cu.getWeakReference(doc);
|
||
this._winRef = Cu.getWeakReference(win);
|
||
this._innerWindowId = win.windowGlobalChild.innerWindowId;
|
||
|
||
this._article = null;
|
||
this._languagePromise = new Promise(resolve => {
|
||
this._foundLanguage = resolve;
|
||
});
|
||
|
||
if (articlePromise) {
|
||
this._articlePromise = articlePromise;
|
||
}
|
||
|
||
this._headerElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-header")
|
||
);
|
||
this._domainElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-domain")
|
||
);
|
||
this._titleElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-title")
|
||
);
|
||
this._readTimeElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-estimated-time")
|
||
);
|
||
this._creditsElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-credits")
|
||
);
|
||
this._contentElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".moz-reader-content")
|
||
);
|
||
this._toolbarContainerElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".toolbar-container")
|
||
);
|
||
this._toolbarElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-controls")
|
||
);
|
||
this._messageElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".reader-message")
|
||
);
|
||
this._containerElementRef = Cu.getWeakReference(
|
||
doc.querySelector(".container")
|
||
);
|
||
|
||
doc.addEventListener("mousedown", this);
|
||
doc.addEventListener("click", this);
|
||
doc.addEventListener("touchstart", this);
|
||
|
||
win.addEventListener("pagehide", this);
|
||
win.addEventListener("resize", this);
|
||
win.addEventListener("wheel", this, { passive: false });
|
||
|
||
this.colorSchemeMediaList = win.matchMedia("(prefers-color-scheme: dark)");
|
||
this.colorSchemeMediaList.addEventListener("change", this);
|
||
|
||
this.forcedColorsMediaList = win.matchMedia("(forced-colors)");
|
||
this.forcedColorsMediaList.addEventListener("change", this);
|
||
|
||
this._topScrollChange = this._topScrollChange.bind(this);
|
||
this._intersectionObs = new win.IntersectionObserver(this._topScrollChange, {
|
||
root: null,
|
||
threshold: [0, 1],
|
||
});
|
||
this._intersectionObs.observe(doc.querySelector(".top-anchor"));
|
||
|
||
this._ctaIntersectionObserver = new win.IntersectionObserver(
|
||
this._pocketCTAObserved.bind(this),
|
||
{
|
||
threshold: 0.5,
|
||
}
|
||
);
|
||
|
||
Services.obs.addObserver(this, "inner-window-destroyed");
|
||
|
||
this._setupButton("close-button", this._onReaderClose.bind(this));
|
||
|
||
// we're ready for any external setup, send a signal for that.
|
||
this._actor.sendAsyncMessage("Reader:OnSetup");
|
||
|
||
let colorSchemeValues = JSON.parse(
|
||
Services.prefs.getCharPref("reader.color_scheme.values")
|
||
);
|
||
let colorSchemeOptions = colorSchemeValues.map(value => ({
|
||
l10nId: COLORSCHEME_L10N_IDS[value],
|
||
groupName: "color-scheme",
|
||
value,
|
||
itemClass: value + "-button",
|
||
}));
|
||
let colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||
|
||
this._setupSegmentedButton(
|
||
"color-scheme-buttons",
|
||
colorSchemeOptions,
|
||
colorScheme,
|
||
this._setColorSchemePref.bind(this)
|
||
);
|
||
this._setColorSchemePref(colorScheme);
|
||
|
||
let fontTypeOptions = [
|
||
{
|
||
l10nId: "about-reader-font-type-sans-serif",
|
||
groupName: "font-type",
|
||
value: "sans-serif",
|
||
itemClass: "sans-serif-button",
|
||
},
|
||
{
|
||
l10nId: "about-reader-font-type-serif",
|
||
groupName: "font-type",
|
||
value: "serif",
|
||
itemClass: "serif-button",
|
||
},
|
||
];
|
||
|
||
let fontType = Services.prefs.getCharPref("reader.font_type");
|
||
this._setupSegmentedButton(
|
||
"font-type-buttons",
|
||
fontTypeOptions,
|
||
fontType,
|
||
this._setFontType.bind(this)
|
||
);
|
||
this._setFontType(fontType);
|
||
|
||
this._setupFontSizeButtons();
|
||
|
||
this._setupContentWidthButtons();
|
||
|
||
this._setupLineHeightButtons();
|
||
|
||
if (win.speechSynthesis && Services.prefs.getBoolPref("narrate.enabled")) {
|
||
new lazy.NarrateControls(win, this._languagePromise);
|
||
}
|
||
|
||
this._loadArticle(docContentType);
|
||
};
|
||
|
||
AboutReader.prototype = {
|
||
_BLOCK_IMAGES_SELECTOR:
|
||
".content p > img:only-child, " +
|
||
".content p > a:only-child > img:only-child, " +
|
||
".content .wp-caption img, " +
|
||
".content figure img",
|
||
|
||
_TABLES_SELECTOR: ".content table",
|
||
|
||
FONT_SIZE_MIN: 1,
|
||
|
||
FONT_SIZE_LEGACY_MAX: 9,
|
||
|
||
FONT_SIZE_MAX: 15,
|
||
|
||
FONT_SIZE_EXTENDED_VALUES: [32, 40, 56, 72, 96, 128],
|
||
|
||
get _doc() {
|
||
return this._docRef.get();
|
||
},
|
||
|
||
get _win() {
|
||
return this._winRef.get();
|
||
},
|
||
|
||
get _headerElement() {
|
||
return this._headerElementRef.get();
|
||
},
|
||
|
||
get _domainElement() {
|
||
return this._domainElementRef.get();
|
||
},
|
||
|
||
get _titleElement() {
|
||
return this._titleElementRef.get();
|
||
},
|
||
|
||
get _readTimeElement() {
|
||
return this._readTimeElementRef.get();
|
||
},
|
||
|
||
get _creditsElement() {
|
||
return this._creditsElementRef.get();
|
||
},
|
||
|
||
get _contentElement() {
|
||
return this._contentElementRef.get();
|
||
},
|
||
|
||
get _toolbarElement() {
|
||
return this._toolbarElementRef.get();
|
||
},
|
||
|
||
get _toolbarContainerElement() {
|
||
return this._toolbarContainerElementRef.get();
|
||
},
|
||
|
||
get _messageElement() {
|
||
return this._messageElementRef.get();
|
||
},
|
||
|
||
get _containerElement() {
|
||
return this._containerElementRef.get();
|
||
},
|
||
|
||
get _isToolbarVertical() {
|
||
if (this._toolbarVertical !== undefined) {
|
||
return this._toolbarVertical;
|
||
}
|
||
return (this._toolbarVertical = Services.prefs.getBoolPref(
|
||
"reader.toolbar.vertical"
|
||
));
|
||
},
|
||
|
||
receiveMessage({ data, name }) {
|
||
const doc = this._doc;
|
||
switch (name) {
|
||
case "Reader:AddButton": {
|
||
if (data.id && data.image && !doc.getElementsByClassName(data.id)[0]) {
|
||
let btn = doc.createElement("button");
|
||
btn.dataset.buttonid = data.id;
|
||
btn.dataset.telemetryId = `reader-${data.telemetryId}`;
|
||
btn.className = "toolbar-button " + data.id;
|
||
btn.setAttribute("aria-labelledby", "label-" + data.id);
|
||
let tip = doc.createElement("span");
|
||
tip.className = "hover-label";
|
||
tip.id = "label-" + data.id;
|
||
doc.l10n.setAttributes(tip, data.l10nId);
|
||
btn.append(tip);
|
||
btn.style.backgroundImage = "url('" + data.image + "')";
|
||
if (data.width && data.height) {
|
||
btn.style.backgroundSize = `${data.width}px ${data.height}px`;
|
||
}
|
||
let tb = this._toolbarElement;
|
||
tb.appendChild(btn);
|
||
this._setupButton(data.id, button => {
|
||
this._actor.sendAsyncMessage(
|
||
"Reader:Clicked-" + button.dataset.buttonid,
|
||
{ article: this._article }
|
||
);
|
||
});
|
||
}
|
||
break;
|
||
}
|
||
case "Reader:RemoveButton": {
|
||
if (data.id) {
|
||
let btn = doc.getElementsByClassName(data.id)[0];
|
||
if (btn) {
|
||
btn.remove();
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case "Reader:ZoomIn": {
|
||
this._changeFontSize(+1);
|
||
break;
|
||
}
|
||
case "Reader:ZoomOut": {
|
||
this._changeFontSize(-1);
|
||
break;
|
||
}
|
||
case "Reader:ResetZoom": {
|
||
this._resetFontSize();
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
handleEvent(aEvent) {
|
||
if (!aEvent.isTrusted) {
|
||
return;
|
||
}
|
||
|
||
let target = aEvent.target;
|
||
switch (aEvent.type) {
|
||
case "touchstart":
|
||
/* fall through */
|
||
case "mousedown":
|
||
if (
|
||
!target.closest(".dropdown-popup") &&
|
||
// Skip handling the toggle button here becase
|
||
// the dropdown will get toggled with the 'click' event.
|
||
!target.classList.contains("dropdown-toggle")
|
||
) {
|
||
this._closeDropdowns();
|
||
}
|
||
break;
|
||
case "click":
|
||
const buttonLabel = target.attributes.getNamedItem(`data-telemetry-id`)
|
||
?.value;
|
||
|
||
if (buttonLabel) {
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"button",
|
||
"click",
|
||
null,
|
||
{
|
||
label: buttonLabel,
|
||
}
|
||
);
|
||
}
|
||
|
||
if (target.classList.contains("dropdown-toggle")) {
|
||
this._toggleDropdownClicked(aEvent);
|
||
}
|
||
break;
|
||
case "scroll":
|
||
let lastHeight = this._lastHeight;
|
||
let { windowUtils } = this._win;
|
||
this._lastHeight = windowUtils.getBoundsWithoutFlushing(
|
||
this._doc.body
|
||
).height;
|
||
// Only close dropdowns if the scroll events are not a result of line
|
||
// height / font-size changes that caused a page height change.
|
||
if (lastHeight == this._lastHeight) {
|
||
this._closeDropdowns(true);
|
||
}
|
||
|
||
break;
|
||
case "resize":
|
||
this._updateImageMargins();
|
||
this._scheduleToolbarOverlapHandler();
|
||
break;
|
||
|
||
case "wheel":
|
||
let doZoom =
|
||
(aEvent.ctrlKey && zoomOnCtrl) || (aEvent.metaKey && zoomOnMeta);
|
||
if (!doZoom) {
|
||
return;
|
||
}
|
||
aEvent.preventDefault();
|
||
|
||
// Throttle events to once per 150ms. This avoids excessively fast zooming.
|
||
if (aEvent.timeStamp <= this._zoomBackoffTime) {
|
||
return;
|
||
}
|
||
this._zoomBackoffTime = aEvent.timeStamp + 150;
|
||
|
||
// Determine the direction of the delta (we don't care about its size);
|
||
// This code is adapted from normalizeWheelEventDelta in
|
||
// toolkt/components/pdfjs/content/web/viewer.js
|
||
let delta = Math.abs(aEvent.deltaX) + Math.abs(aEvent.deltaY);
|
||
let angle = Math.atan2(aEvent.deltaY, aEvent.deltaX);
|
||
if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
|
||
delta = -delta;
|
||
}
|
||
|
||
if (delta > 0) {
|
||
this._changeFontSize(+1);
|
||
} else if (delta < 0) {
|
||
this._changeFontSize(-1);
|
||
}
|
||
break;
|
||
|
||
case "pagehide":
|
||
this._closeDropdowns();
|
||
|
||
this._actor.readerModeHidden();
|
||
this.clearActor();
|
||
|
||
// Disconnect and delete IntersectionObservers to prevent memory leaks:
|
||
|
||
this._intersectionObs.unobserve(this._doc.querySelector(".top-anchor"));
|
||
this._ctaIntersectionObserver.disconnect();
|
||
|
||
delete this._intersectionObs;
|
||
delete this._ctaIntersectionObserver;
|
||
|
||
break;
|
||
|
||
case "change":
|
||
let colorScheme;
|
||
if (this.forcedColorsMediaList.matches) {
|
||
colorScheme = "hcm";
|
||
} else {
|
||
colorScheme = Services.prefs.getCharPref("reader.color_scheme");
|
||
// We should be changing the color scheme in relation to a preference change
|
||
// if the user has the color scheme preference set to "Auto".
|
||
if (colorScheme == "auto") {
|
||
colorScheme = this.colorSchemeMediaList.matches ? "dark" : "light";
|
||
}
|
||
}
|
||
this._setColorScheme(colorScheme);
|
||
|
||
break;
|
||
}
|
||
},
|
||
|
||
clearActor() {
|
||
this._actor = null;
|
||
},
|
||
|
||
_onReaderClose() {
|
||
if (this._actor) {
|
||
this._actor.closeReaderMode();
|
||
}
|
||
},
|
||
|
||
async _resetFontSize() {
|
||
await lazy.AsyncPrefs.reset("reader.font_size");
|
||
let currentSize = Services.prefs.getIntPref("reader.font_size");
|
||
this._setFontSize(currentSize);
|
||
},
|
||
|
||
_setFontSize(newFontSize) {
|
||
this._fontSize = Math.min(
|
||
this.FONT_SIZE_MAX,
|
||
Math.max(this.FONT_SIZE_MIN, newFontSize)
|
||
);
|
||
let size;
|
||
if (this._fontSize > this.FONT_SIZE_LEGACY_MAX) {
|
||
// -1 because we're indexing into a 0-indexed array, so the first value
|
||
// over the legacy max should be 0, the next 1, etc.
|
||
let index = this._fontSize - this.FONT_SIZE_LEGACY_MAX - 1;
|
||
size = this.FONT_SIZE_EXTENDED_VALUES[index];
|
||
} else {
|
||
size = 10 + 2 * this._fontSize;
|
||
}
|
||
|
||
let readerBody = this._doc.body;
|
||
readerBody.style.setProperty("--font-size", size + "px");
|
||
return lazy.AsyncPrefs.set("reader.font_size", this._fontSize);
|
||
},
|
||
|
||
_setupFontSizeButtons() {
|
||
let plusButton = this._doc.querySelector(".plus-button");
|
||
let minusButton = this._doc.querySelector(".minus-button");
|
||
|
||
let currentSize = Services.prefs.getIntPref("reader.font_size");
|
||
this._setFontSize(currentSize);
|
||
this._updateFontSizeButtonControls();
|
||
|
||
plusButton.addEventListener(
|
||
"click",
|
||
event => {
|
||
if (!event.isTrusted) {
|
||
return;
|
||
}
|
||
event.stopPropagation();
|
||
this._changeFontSize(+1);
|
||
},
|
||
true
|
||
);
|
||
|
||
minusButton.addEventListener(
|
||
"click",
|
||
event => {
|
||
if (!event.isTrusted) {
|
||
return;
|
||
}
|
||
event.stopPropagation();
|
||
this._changeFontSize(-1);
|
||
},
|
||
true
|
||
);
|
||
},
|
||
|
||
_updateFontSizeButtonControls() {
|
||
let plusButton = this._doc.querySelector(".plus-button");
|
||
let minusButton = this._doc.querySelector(".minus-button");
|
||
|
||
let currentSize = this._fontSize;
|
||
let fontValue = this._doc.querySelector(".font-size-value");
|
||
fontValue.textContent = currentSize;
|
||
|
||
if (currentSize === this.FONT_SIZE_MIN) {
|
||
minusButton.setAttribute("disabled", true);
|
||
} else {
|
||
minusButton.removeAttribute("disabled");
|
||
}
|
||
if (currentSize === this.FONT_SIZE_MAX) {
|
||
plusButton.setAttribute("disabled", true);
|
||
} else {
|
||
plusButton.removeAttribute("disabled");
|
||
}
|
||
},
|
||
|
||
_changeFontSize(changeAmount) {
|
||
let currentSize =
|
||
Services.prefs.getIntPref("reader.font_size") + changeAmount;
|
||
this._setFontSize(currentSize);
|
||
this._updateFontSizeButtonControls();
|
||
this._scheduleToolbarOverlapHandler();
|
||
},
|
||
|
||
_setContentWidth(newContentWidth) {
|
||
this._contentWidth = newContentWidth;
|
||
this._displayContentWidth(newContentWidth);
|
||
let width = 20 + 5 * (this._contentWidth - 1) + "em";
|
||
this._doc.body.style.setProperty("--content-width", width);
|
||
this._scheduleToolbarOverlapHandler();
|
||
return lazy.AsyncPrefs.set("reader.content_width", this._contentWidth);
|
||
},
|
||
|
||
_displayContentWidth(currentContentWidth) {
|
||
let contentWidthValue = this._doc.querySelector(".content-width-value");
|
||
contentWidthValue.textContent = currentContentWidth;
|
||
},
|
||
|
||
_setupContentWidthButtons() {
|
||
const CONTENT_WIDTH_MIN = 1;
|
||
const CONTENT_WIDTH_MAX = 9;
|
||
|
||
let currentContentWidth = Services.prefs.getIntPref("reader.content_width");
|
||
currentContentWidth = Math.max(
|
||
CONTENT_WIDTH_MIN,
|
||
Math.min(CONTENT_WIDTH_MAX, currentContentWidth)
|
||
);
|
||
|
||
this._displayContentWidth(currentContentWidth);
|
||
|
||
let plusButton = this._doc.querySelector(".content-width-plus-button");
|
||
let minusButton = this._doc.querySelector(".content-width-minus-button");
|
||
|
||
function updateControls() {
|
||
if (currentContentWidth === CONTENT_WIDTH_MIN) {
|
||
minusButton.setAttribute("disabled", true);
|
||
} else {
|
||
minusButton.removeAttribute("disabled");
|
||
}
|
||
if (currentContentWidth === CONTENT_WIDTH_MAX) {
|
||
plusButton.setAttribute("disabled", true);
|
||
} else {
|
||
plusButton.removeAttribute("disabled");
|
||
}
|
||
}
|
||
|
||
updateControls();
|
||
this._setContentWidth(currentContentWidth);
|
||
|
||
plusButton.addEventListener(
|
||
"click",
|
||
event => {
|
||
if (!event.isTrusted) {
|
||
return;
|
||
}
|
||
event.stopPropagation();
|
||
|
||
if (currentContentWidth >= CONTENT_WIDTH_MAX) {
|
||
return;
|
||
}
|
||
|
||
currentContentWidth++;
|
||
updateControls();
|
||
this._setContentWidth(currentContentWidth);
|
||
},
|
||
true
|
||
);
|
||
|
||
minusButton.addEventListener(
|
||
"click",
|
||
event => {
|
||
if (!event.isTrusted) {
|
||
return;
|
||
}
|
||
event.stopPropagation();
|
||
|
||
if (currentContentWidth <= CONTENT_WIDTH_MIN) {
|
||
return;
|
||
}
|
||
|
||
currentContentWidth--;
|
||
updateControls();
|
||
this._setContentWidth(currentContentWidth);
|
||
},
|
||
true
|
||
);
|
||
},
|
||
|
||
_setLineHeight(newLineHeight) {
|
||
this._displayLineHeight(newLineHeight);
|
||
let height = 1 + 0.2 * (newLineHeight - 1) + "em";
|
||
this._containerElement.style.setProperty("--line-height", height);
|
||
return lazy.AsyncPrefs.set("reader.line_height", newLineHeight);
|
||
},
|
||
|
||
_displayLineHeight(currentLineHeight) {
|
||
let lineHeightValue = this._doc.querySelector(".line-height-value");
|
||
lineHeightValue.textContent = currentLineHeight;
|
||
},
|
||
|
||
_setupLineHeightButtons() {
|
||
const LINE_HEIGHT_MIN = 1;
|
||
const LINE_HEIGHT_MAX = 9;
|
||
|
||
let currentLineHeight = Services.prefs.getIntPref("reader.line_height");
|
||
currentLineHeight = Math.max(
|
||
LINE_HEIGHT_MIN,
|
||
Math.min(LINE_HEIGHT_MAX, currentLineHeight)
|
||
);
|
||
|
||
this._displayLineHeight(currentLineHeight);
|
||
|
||
let plusButton = this._doc.querySelector(".line-height-plus-button");
|
||
let minusButton = this._doc.querySelector(".line-height-minus-button");
|
||
|
||
function updateControls() {
|
||
if (currentLineHeight === LINE_HEIGHT_MIN) {
|
||
minusButton.setAttribute("disabled", true);
|
||
} else {
|
||
minusButton.removeAttribute("disabled");
|
||
}
|
||
if (currentLineHeight === LINE_HEIGHT_MAX) {
|
||
plusButton.setAttribute("disabled", true);
|
||
} else {
|
||
plusButton.removeAttribute("disabled");
|
||
}
|
||
}
|
||
|
||
updateControls();
|
||
this._setLineHeight(currentLineHeight);
|
||
|
||
plusButton.addEventListener(
|
||
"click",
|
||
event => {
|
||
if (!event.isTrusted) {
|
||
return;
|
||
}
|
||
event.stopPropagation();
|
||
|
||
if (currentLineHeight >= LINE_HEIGHT_MAX) {
|
||
return;
|
||
}
|
||
|
||
currentLineHeight++;
|
||
updateControls();
|
||
this._setLineHeight(currentLineHeight);
|
||
},
|
||
true
|
||
);
|
||
|
||
minusButton.addEventListener(
|
||
"click",
|
||
event => {
|
||
if (!event.isTrusted) {
|
||
return;
|
||
}
|
||
event.stopPropagation();
|
||
|
||
if (currentLineHeight <= LINE_HEIGHT_MIN) {
|
||
return;
|
||
}
|
||
|
||
currentLineHeight--;
|
||
updateControls();
|
||
this._setLineHeight(currentLineHeight);
|
||
},
|
||
true
|
||
);
|
||
},
|
||
|
||
_setColorScheme(newColorScheme) {
|
||
// There's nothing to change if the new color scheme is the same as our current scheme.
|
||
if (this._colorScheme === newColorScheme) {
|
||
return;
|
||
}
|
||
|
||
let bodyClasses = this._doc.body.classList;
|
||
|
||
if (this._colorScheme) {
|
||
bodyClasses.remove(this._colorScheme);
|
||
}
|
||
|
||
if (!this._win.matchMedia("(forced-colors)").matches) {
|
||
if (newColorScheme === "auto") {
|
||
this._colorScheme = this.colorSchemeMediaList.matches
|
||
? "dark"
|
||
: "light";
|
||
} else {
|
||
this._colorScheme = newColorScheme;
|
||
}
|
||
} else {
|
||
this._colorScheme = "hcm";
|
||
}
|
||
|
||
bodyClasses.add(this._colorScheme);
|
||
},
|
||
|
||
// Pref values include "dark", "light", "sepia", and "auto"
|
||
_setColorSchemePref(colorSchemePref) {
|
||
this._setColorScheme(colorSchemePref);
|
||
|
||
lazy.AsyncPrefs.set("reader.color_scheme", colorSchemePref);
|
||
},
|
||
|
||
_setFontType(newFontType) {
|
||
if (this._fontType === newFontType) {
|
||
return;
|
||
}
|
||
|
||
let bodyClasses = this._doc.body.classList;
|
||
|
||
if (this._fontType) {
|
||
bodyClasses.remove(this._fontType);
|
||
}
|
||
|
||
this._fontType = newFontType;
|
||
bodyClasses.add(this._fontType);
|
||
|
||
lazy.AsyncPrefs.set("reader.font_type", this._fontType);
|
||
},
|
||
|
||
async _loadArticle(docContentType = "document") {
|
||
let url = this._getOriginalUrl();
|
||
this._showProgressDelayed();
|
||
|
||
let article;
|
||
if (this._articlePromise) {
|
||
article = await this._articlePromise;
|
||
}
|
||
|
||
if (!article) {
|
||
try {
|
||
article = await ReaderMode.downloadAndParseDocument(
|
||
url,
|
||
docContentType
|
||
);
|
||
} catch (e) {
|
||
if (e?.newURL && this._actor) {
|
||
await this._actor.sendQuery("RedirectTo", {
|
||
newURL: e.newURL,
|
||
article: e.article,
|
||
});
|
||
|
||
let readerURL = "about:reader?url=" + encodeURIComponent(e.newURL);
|
||
this._win.location.replace(readerURL);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!this._actor) {
|
||
return;
|
||
}
|
||
|
||
// Replace the loading message with an error message if there's a failure.
|
||
// Users are supposed to navigate away by themselves (because we cannot
|
||
// remove ourselves from session history.)
|
||
if (!article) {
|
||
this._showError();
|
||
return;
|
||
}
|
||
|
||
this._showContent(article);
|
||
},
|
||
|
||
async _requestPocketLoginStatus() {
|
||
let isLoggedIn = await this._actor.sendQuery(
|
||
"Reader:PocketLoginStatusRequest"
|
||
);
|
||
|
||
return isLoggedIn;
|
||
},
|
||
|
||
async _requestPocketArticleInfo(url) {
|
||
let articleInfo = await this._actor.sendQuery(
|
||
"Reader:PocketGetArticleInfo",
|
||
{
|
||
url,
|
||
}
|
||
);
|
||
|
||
return articleInfo?.item_preview?.item_id;
|
||
},
|
||
|
||
async _requestPocketArticleRecs(itemID) {
|
||
let recs = await this._actor.sendQuery("Reader:PocketGetArticleRecs", {
|
||
itemID,
|
||
});
|
||
|
||
return recs;
|
||
},
|
||
|
||
async _savePocketArticle(url) {
|
||
let result = await this._actor.sendQuery("Reader:PocketSaveArticle", {
|
||
url,
|
||
});
|
||
|
||
return result;
|
||
},
|
||
|
||
async _requestFavicon() {
|
||
let iconDetails = await this._actor.sendQuery("Reader:FaviconRequest", {
|
||
url: this._article.url,
|
||
preferredWidth: 16 * this._win.devicePixelRatio,
|
||
});
|
||
|
||
if (iconDetails) {
|
||
this._loadFavicon(iconDetails.url, iconDetails.faviconUrl);
|
||
}
|
||
},
|
||
|
||
_loadFavicon(url, faviconUrl) {
|
||
if (this._article.url !== url) {
|
||
return;
|
||
}
|
||
|
||
let doc = this._doc;
|
||
|
||
let link = doc.createElement("link");
|
||
link.rel = "shortcut icon";
|
||
link.href = faviconUrl;
|
||
|
||
doc.getElementsByTagName("head")[0].appendChild(link);
|
||
},
|
||
|
||
_updateImageMargins() {
|
||
let windowWidth = this._win.innerWidth;
|
||
let bodyWidth = this._doc.body.clientWidth;
|
||
|
||
let setImageMargins = function(img) {
|
||
img.classList.add("moz-reader-block-img");
|
||
|
||
// If the image is at least as wide as the window, make it fill edge-to-edge on mobile.
|
||
if (img.naturalWidth >= windowWidth) {
|
||
img.setAttribute("moz-reader-full-width", true);
|
||
} else {
|
||
img.removeAttribute("moz-reader-full-width");
|
||
}
|
||
|
||
// If the image is at least half as wide as the body, center it on desktop.
|
||
if (img.naturalWidth >= bodyWidth / 2) {
|
||
img.setAttribute("moz-reader-center", true);
|
||
} else {
|
||
img.removeAttribute("moz-reader-center");
|
||
}
|
||
};
|
||
|
||
let imgs = this._doc.querySelectorAll(this._BLOCK_IMAGES_SELECTOR);
|
||
for (let i = imgs.length; --i >= 0; ) {
|
||
let img = imgs[i];
|
||
|
||
if (img.naturalWidth > 0) {
|
||
setImageMargins(img);
|
||
} else {
|
||
img.onload = function() {
|
||
setImageMargins(img);
|
||
};
|
||
}
|
||
}
|
||
},
|
||
|
||
_updateWideTables() {
|
||
let windowWidth = this._win.innerWidth;
|
||
|
||
// Avoid horizontal overflow in the document by making tables that are wider than half browser window's size
|
||
// by making it scrollable.
|
||
let tables = this._doc.querySelectorAll(this._TABLES_SELECTOR);
|
||
for (let i = tables.length; --i >= 0; ) {
|
||
let table = tables[i];
|
||
let rect = table.getBoundingClientRect();
|
||
let tableWidth = rect.width;
|
||
|
||
if (windowWidth / 2 <= tableWidth) {
|
||
table.classList.add("moz-reader-wide-table");
|
||
}
|
||
}
|
||
},
|
||
|
||
_maybeSetTextDirection: function Read_maybeSetTextDirection(article) {
|
||
// Set the article's "dir" on the contents.
|
||
// If no direction is specified, the contents should automatically be LTR
|
||
// regardless of the UI direction to avoid inheriting the parent's direction
|
||
// if the UI is RTL.
|
||
this._containerElement.dir = article.dir || "ltr";
|
||
|
||
// The native locale could be set differently than the article's text direction.
|
||
this._readTimeElement.dir = isAppLocaleRTL ? "rtl" : "ltr";
|
||
|
||
// This is used to mirror the line height buttons in the toolbar, when relevant.
|
||
this._toolbarElement.setAttribute("articledir", article.dir || "ltr");
|
||
},
|
||
|
||
_showError() {
|
||
this._headerElement.classList.remove("reader-show-element");
|
||
this._contentElement.classList.remove("reader-show-element");
|
||
|
||
this._doc.l10n.setAttributes(
|
||
this._messageElement,
|
||
"about-reader-load-error"
|
||
);
|
||
this._doc.l10n.setAttributes(
|
||
this._doc.getElementById("reader-title"),
|
||
"about-reader-load-error"
|
||
);
|
||
this._messageElement.style.display = "block";
|
||
|
||
this._doc.documentElement.dataset.isError = true;
|
||
|
||
this._error = true;
|
||
|
||
this._doc.dispatchEvent(
|
||
new this._win.CustomEvent("AboutReaderContentError", {
|
||
bubbles: true,
|
||
cancelable: false,
|
||
})
|
||
);
|
||
},
|
||
|
||
// This function is the JS version of Java's StringUtils.stripCommonSubdomains.
|
||
_stripHost(host) {
|
||
if (!host) {
|
||
return host;
|
||
}
|
||
|
||
let start = 0;
|
||
|
||
if (host.startsWith("www.")) {
|
||
start = 4;
|
||
} else if (host.startsWith("m.")) {
|
||
start = 2;
|
||
} else if (host.startsWith("mobile.")) {
|
||
start = 7;
|
||
}
|
||
|
||
return host.substring(start);
|
||
},
|
||
|
||
_showContent(article) {
|
||
this._messageElement.classList.remove("reader-show-element");
|
||
|
||
this._article = article;
|
||
|
||
this._domainElement.href = article.url;
|
||
let articleUri = Services.io.newURI(article.url);
|
||
|
||
try {
|
||
this._domainElement.textContent = this._stripHost(articleUri.host);
|
||
} catch (ex) {
|
||
let url = this._actor.document.URL;
|
||
url = url.substring(url.indexOf("%2F") + 6);
|
||
url = url.substring(0, url.indexOf("%2F"));
|
||
|
||
this._domainElement.textContent = url;
|
||
}
|
||
|
||
this._creditsElement.textContent = article.byline;
|
||
|
||
this._titleElement.textContent = article.title;
|
||
|
||
// TODO: Once formatRange() and selectRange() are available outside Nightly,
|
||
// use them here. https://bugzilla.mozilla.org/show_bug.cgi?id=1795317
|
||
const slow = article.readingTimeMinsSlow;
|
||
const fast = article.readingTimeMinsFast;
|
||
const fastStr = lazy.numberFormat.format(fast);
|
||
const slowStr = lazy.numberFormat.format(slow);
|
||
this._doc.l10n.setAttributes(
|
||
this._readTimeElement,
|
||
"about-reader-estimated-read-time",
|
||
{
|
||
range: fastStr === slowStr ? `~${fastStr}` : `${fastStr}–${slowStr}`,
|
||
rangePlural: lazy.pluralRules.select(slow),
|
||
}
|
||
);
|
||
|
||
// If a document title was not provided in the constructor, we'll fall back
|
||
// to using the article title.
|
||
if (!this._doc.title) {
|
||
this._doc.title = article.title;
|
||
}
|
||
|
||
this._containerElement.setAttribute("lang", article.lang);
|
||
|
||
this._headerElement.classList.add("reader-show-element");
|
||
|
||
let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(
|
||
Ci.nsIParserUtils
|
||
);
|
||
let contentFragment = parserUtils.parseFragment(
|
||
article.content,
|
||
Ci.nsIParserUtils.SanitizerDropForms |
|
||
Ci.nsIParserUtils.SanitizerAllowStyle,
|
||
false,
|
||
articleUri,
|
||
this._contentElement
|
||
);
|
||
this._contentElement.innerHTML = "";
|
||
this._contentElement.appendChild(contentFragment);
|
||
this._maybeSetTextDirection(article);
|
||
this._foundLanguage(article.language);
|
||
|
||
this._contentElement.classList.add("reader-show-element");
|
||
this._updateImageMargins();
|
||
this._updateWideTables();
|
||
|
||
this._requestFavicon();
|
||
this._doc.body.classList.add("loaded");
|
||
|
||
this._goToReference(articleUri.ref);
|
||
|
||
Services.obs.notifyObservers(this._win, "AboutReader:Ready");
|
||
|
||
this._doc.dispatchEvent(
|
||
new this._win.CustomEvent("AboutReaderContentReady", {
|
||
bubbles: true,
|
||
cancelable: false,
|
||
})
|
||
);
|
||
|
||
// Show Pocket CTA block after article has loaded to prevent it flashing in prematurely
|
||
this._setupPocketCTA();
|
||
},
|
||
|
||
_hideContent() {
|
||
this._headerElement.classList.remove("reader-show-element");
|
||
this._contentElement.classList.remove("reader-show-element");
|
||
},
|
||
|
||
_showProgressDelayed() {
|
||
this._win.setTimeout(() => {
|
||
// No need to show progress if the article has been loaded,
|
||
// if the window has been unloaded, or if there was an error
|
||
// trying to load the article.
|
||
if (this._article || !this._actor || this._error) {
|
||
return;
|
||
}
|
||
|
||
this._headerElement.classList.remove("reader-show-element");
|
||
this._contentElement.classList.remove("reader-show-element");
|
||
|
||
this._doc.l10n.setAttributes(
|
||
this._messageElement,
|
||
"about-reader-loading"
|
||
);
|
||
this._messageElement.classList.add("reader-show-element");
|
||
}, 300);
|
||
},
|
||
|
||
/**
|
||
* Returns the original article URL for this about:reader view.
|
||
*/
|
||
_getOriginalUrl(win) {
|
||
let url = win ? win.location.href : this._win.location.href;
|
||
return ReaderMode.getOriginalUrl(url) || url;
|
||
},
|
||
|
||
_setupSegmentedButton(id, options, initialValue, callback) {
|
||
let doc = this._doc;
|
||
let segmentedButton = doc.getElementsByClassName(id)[0];
|
||
|
||
for (let option of options) {
|
||
let radioButton = doc.createElement("input");
|
||
radioButton.id = "radio-item" + option.itemClass;
|
||
radioButton.type = "radio";
|
||
radioButton.classList.add("radio-button");
|
||
radioButton.name = option.groupName;
|
||
segmentedButton.appendChild(radioButton);
|
||
|
||
let item = doc.createElement("label");
|
||
item.htmlFor = radioButton.id;
|
||
item.classList.add(option.itemClass);
|
||
doc.l10n.setAttributes(item, option.l10nId);
|
||
|
||
segmentedButton.appendChild(item);
|
||
|
||
radioButton.addEventListener(
|
||
"input",
|
||
function(aEvent) {
|
||
if (!aEvent.isTrusted) {
|
||
return;
|
||
}
|
||
|
||
let labels = segmentedButton.children;
|
||
for (let label of labels) {
|
||
label.removeAttribute("checked");
|
||
}
|
||
|
||
aEvent.target.nextElementSibling.setAttribute("checked", "true");
|
||
callback(option.value);
|
||
},
|
||
true
|
||
);
|
||
|
||
if (option.value === initialValue) {
|
||
radioButton.checked = true;
|
||
item.setAttribute("checked", "true");
|
||
}
|
||
}
|
||
},
|
||
|
||
_setupButton(id, callback) {
|
||
let button = this._doc.querySelector("." + id);
|
||
button.removeAttribute("hidden");
|
||
button.addEventListener(
|
||
"click",
|
||
function(aEvent) {
|
||
if (!aEvent.isTrusted) {
|
||
return;
|
||
}
|
||
|
||
let btn = aEvent.target;
|
||
callback(btn);
|
||
},
|
||
true
|
||
);
|
||
},
|
||
|
||
_toggleDropdownClicked(event) {
|
||
let dropdown = event.target.closest(".dropdown");
|
||
|
||
if (!dropdown) {
|
||
return;
|
||
}
|
||
|
||
event.stopPropagation();
|
||
|
||
if (dropdown.classList.contains("open")) {
|
||
this._closeDropdowns();
|
||
} else {
|
||
this._openDropdown(dropdown);
|
||
}
|
||
},
|
||
|
||
/*
|
||
* If the ReaderView banner font-dropdown is closed, open it.
|
||
*/
|
||
_openDropdown(dropdown, window) {
|
||
if (dropdown.classList.contains("open")) {
|
||
return;
|
||
}
|
||
|
||
this._closeDropdowns();
|
||
|
||
// Get the height of the doc and start handling scrolling:
|
||
let { windowUtils } = this._win;
|
||
this._lastHeight = windowUtils.getBoundsWithoutFlushing(
|
||
this._doc.body
|
||
).height;
|
||
this._doc.addEventListener("scroll", this);
|
||
|
||
dropdown.classList.add("open");
|
||
this._toolbarElement.classList.add("dropdown-open");
|
||
|
||
this._toolbarContainerElement.classList.add("dropdown-open");
|
||
this._toggleToolbarFixedPosition(true);
|
||
},
|
||
|
||
/*
|
||
* If the ReaderView has open dropdowns, close them. If we are closing the
|
||
* dropdowns because the page is scrolling, allow popups to stay open with
|
||
* the keep-open class.
|
||
*/
|
||
_closeDropdowns(scrolling) {
|
||
let selector = ".dropdown.open";
|
||
if (scrolling) {
|
||
selector += ":not(.keep-open)";
|
||
}
|
||
|
||
let openDropdowns = this._doc.querySelectorAll(selector);
|
||
let haveOpenDropdowns = openDropdowns.length;
|
||
for (let dropdown of openDropdowns) {
|
||
dropdown.classList.remove("open");
|
||
}
|
||
this._toolbarElement.classList.remove("dropdown-open");
|
||
|
||
if (haveOpenDropdowns) {
|
||
this._toolbarContainerElement.classList.remove("dropdown-open");
|
||
this._toggleToolbarFixedPosition(false);
|
||
}
|
||
|
||
// Stop handling scrolling:
|
||
this._doc.removeEventListener("scroll", this);
|
||
},
|
||
|
||
_toggleToolbarFixedPosition(shouldBeFixed) {
|
||
let el = this._toolbarContainerElement;
|
||
let fontSize = this._doc.body.style.getPropertyValue("--font-size");
|
||
let contentWidth = this._doc.body.style.getPropertyValue("--content-width");
|
||
if (shouldBeFixed) {
|
||
el.style.setProperty("--font-size", fontSize);
|
||
el.style.setProperty("--content-width", contentWidth);
|
||
el.classList.add("transition-location");
|
||
} else {
|
||
let expectTransition =
|
||
el.style.getPropertyValue("--font-size") != fontSize ||
|
||
el.style.getPropertyValue("--content-width") != contentWidth;
|
||
if (expectTransition) {
|
||
el.addEventListener(
|
||
"transitionend",
|
||
() => el.classList.remove("transition-location"),
|
||
{ once: true }
|
||
);
|
||
} else {
|
||
el.classList.remove("transition-location");
|
||
}
|
||
el.style.removeProperty("--font-size");
|
||
el.style.removeProperty("--content-width");
|
||
el.classList.remove("overlaps");
|
||
}
|
||
},
|
||
|
||
_scheduleToolbarOverlapHandler() {
|
||
if (this._enqueuedToolbarOverlapHandler) {
|
||
return;
|
||
}
|
||
this._enqueuedToolbarOverlapHandler = this._win.requestAnimationFrame(
|
||
() => {
|
||
this._win.setTimeout(() => this._toolbarOverlapHandler(), 0);
|
||
}
|
||
);
|
||
},
|
||
|
||
_toolbarOverlapHandler() {
|
||
delete this._enqueuedToolbarOverlapHandler;
|
||
// Ensure the dropdown is still open to avoid racing with that changing.
|
||
if (this._toolbarContainerElement.classList.contains("dropdown-open")) {
|
||
let { windowUtils } = this._win;
|
||
let toolbarBounds = windowUtils.getBoundsWithoutFlushing(
|
||
this._toolbarElement.parentNode
|
||
);
|
||
let textBounds = windowUtils.getBoundsWithoutFlushing(
|
||
this._containerElement
|
||
);
|
||
let overlaps = false;
|
||
if (isAppLocaleRTL) {
|
||
overlaps = textBounds.right > toolbarBounds.left;
|
||
} else {
|
||
overlaps = textBounds.left < toolbarBounds.right;
|
||
}
|
||
this._toolbarContainerElement.classList.toggle("overlaps", overlaps);
|
||
}
|
||
},
|
||
|
||
_topScrollChange(entries) {
|
||
if (!entries.length) {
|
||
return;
|
||
}
|
||
// If we don't intersect the item at the top of the document, we're
|
||
// scrolled down:
|
||
let scrolled = !entries[entries.length - 1].isIntersecting;
|
||
let tbc = this._toolbarContainerElement;
|
||
tbc.classList.toggle("scrolled", scrolled);
|
||
},
|
||
|
||
/*
|
||
* Scroll reader view to a reference
|
||
*/
|
||
_goToReference(ref) {
|
||
if (ref) {
|
||
if (this._doc.readyState == "complete") {
|
||
this._win.location.hash = ref;
|
||
} else {
|
||
this._win.addEventListener(
|
||
"load",
|
||
() => {
|
||
this._win.location.hash = ref;
|
||
},
|
||
{ once: true }
|
||
);
|
||
}
|
||
}
|
||
},
|
||
|
||
_enableDismissCTA() {
|
||
let elDismissCta = this._doc.querySelector(`.pocket-dismiss-cta`);
|
||
|
||
elDismissCta?.addEventListener(`click`, e => {
|
||
this._doc.querySelector("#pocket-cta-container").hidden = true;
|
||
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"pocket_cta",
|
||
"close_cta",
|
||
null,
|
||
{}
|
||
);
|
||
});
|
||
},
|
||
|
||
_enableRecShowHide() {
|
||
let elPocketRecs = this._doc.querySelector(`.pocket-recs`);
|
||
let elCollapseRecs = this._doc.querySelector(`.pocket-collapse-recs`);
|
||
let elSignUp = this._doc.querySelector(`div.pocket-sign-up-wrapper`);
|
||
|
||
let toggleRecsVisibility = () => {
|
||
let isClosed = elPocketRecs.classList.contains(`closed`);
|
||
|
||
isClosed = !isClosed; // Toggle
|
||
|
||
if (isClosed) {
|
||
elPocketRecs.classList.add(`closed`);
|
||
elCollapseRecs.classList.add(`closed`);
|
||
elSignUp.setAttribute(`hidden`, true);
|
||
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"pocket_cta",
|
||
"minimize_recs_click",
|
||
null,
|
||
{}
|
||
);
|
||
} else {
|
||
elPocketRecs.classList.remove(`closed`);
|
||
elCollapseRecs.classList.remove(`closed`);
|
||
elSignUp.removeAttribute(`hidden`);
|
||
}
|
||
};
|
||
|
||
elCollapseRecs?.addEventListener(`click`, e => {
|
||
toggleRecsVisibility();
|
||
});
|
||
},
|
||
|
||
_buildPocketRec(title, url, publisher, thumb, time) {
|
||
let fragment = this._doc.createDocumentFragment();
|
||
|
||
let elContainer = this._doc.createElement(`div`);
|
||
let elTitle = this._doc.createElement(`header`);
|
||
let elMetadata = this._doc.createElement(`p`);
|
||
let elThumb = this._doc.createElement(`img`);
|
||
let elSideWrap = this._doc.createElement(`div`);
|
||
let elTop = this._doc.createElement(`a`);
|
||
let elBottom = this._doc.createElement(`div`);
|
||
let elAdd = this._doc.createElement(`button`);
|
||
|
||
elAdd.classList.add(`pocket-btn-add`);
|
||
elBottom.classList.add(`pocket-rec-bottom`);
|
||
elTop.classList.add(`pocket-rec-top`);
|
||
elSideWrap.classList.add(`pocket-rec-side`);
|
||
elContainer.classList.add(`pocket-rec`);
|
||
elTitle.classList.add(`pocket-rec-title`);
|
||
elMetadata.classList.add(`pocket-rec-meta`);
|
||
|
||
elTop.setAttribute(`href`, url);
|
||
|
||
elTop.addEventListener(`click`, e => {
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"pocket_cta",
|
||
"rec_click",
|
||
null,
|
||
{}
|
||
);
|
||
});
|
||
|
||
elThumb.classList.add(`pocket-rec-thumb`);
|
||
elThumb.setAttribute(`loading`, `lazy`);
|
||
elThumb.addEventListener(`load`, () => {
|
||
elThumb.classList.add(`pocket-rec-thumb-loaded`);
|
||
});
|
||
elThumb.setAttribute(
|
||
`src`,
|
||
`https://img-getpocket.cdn.mozilla.net/132x132/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${thumb}`
|
||
);
|
||
|
||
elAdd.textContent = `Save`;
|
||
elTitle.textContent = title;
|
||
|
||
if (publisher && time) {
|
||
elMetadata.textContent = `${publisher} · ${time} min`;
|
||
} else if (publisher) {
|
||
elMetadata.textContent = `${publisher}`;
|
||
} else if (time) {
|
||
elMetadata.textContent = `${time} min`;
|
||
}
|
||
|
||
elSideWrap.appendChild(elTitle);
|
||
elSideWrap.appendChild(elMetadata);
|
||
elTop.appendChild(elSideWrap);
|
||
elTop.appendChild(elThumb);
|
||
elBottom.appendChild(elAdd);
|
||
elContainer.appendChild(elTop);
|
||
elContainer.appendChild(elBottom);
|
||
fragment.appendChild(elContainer);
|
||
|
||
elAdd.addEventListener(`click`, e => {
|
||
this._savePocketArticle(url);
|
||
elAdd.textContent = `Saved`;
|
||
elAdd.classList.add(`saved`);
|
||
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"pocket_cta",
|
||
"rec_saved",
|
||
null,
|
||
{}
|
||
);
|
||
});
|
||
|
||
return fragment;
|
||
},
|
||
|
||
async _getAndBuildPocketRecs() {
|
||
let elTarget = this._doc.querySelector(`.pocket-recs`);
|
||
let url = this._getOriginalUrl();
|
||
let itemID = await this._requestPocketArticleInfo(url);
|
||
let articleRecs = await this._requestPocketArticleRecs(itemID);
|
||
|
||
articleRecs.recommendations.forEach(rec => {
|
||
// Parse a domain from the article URL in case the Publisher name isn't available
|
||
let parsedDomain = new URL(rec.item?.normal_url)?.hostname;
|
||
|
||
// Calculate read time from word count in case it's not available
|
||
let calculatedReadTime = Math.ceil(rec.item?.word_count / 220);
|
||
|
||
let elRec = this._buildPocketRec(
|
||
rec.item?.title,
|
||
rec.item?.normal_url,
|
||
rec.item?.domain_metadata?.name || parsedDomain,
|
||
rec.item?.top_image_url,
|
||
rec.item?.time_to_read || calculatedReadTime
|
||
);
|
||
|
||
elTarget.appendChild(elRec);
|
||
});
|
||
},
|
||
|
||
_pocketCTAObserved(entries) {
|
||
if (entries && entries[0]?.isIntersecting) {
|
||
this._ctaIntersectionObserver.disconnect();
|
||
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"pocket_cta",
|
||
"cta_seen",
|
||
null,
|
||
{
|
||
logged_in: `${this._isLoggedInPocketUser}`,
|
||
}
|
||
);
|
||
}
|
||
},
|
||
|
||
async _setupPocketCTA() {
|
||
let ctaVersion = lazy.NimbusFeatures.readerMode.getAllVariables()
|
||
?.pocketCTAVersion;
|
||
this._isLoggedInPocketUser = await this._requestPocketLoginStatus();
|
||
let elPocketCTAWrapper = this._doc.querySelector("#pocket-cta-container");
|
||
|
||
// Show the Pocket CTA container if the pref is set and valid
|
||
if (ctaVersion === `cta-and-recs` || ctaVersion === `cta-only`) {
|
||
if (ctaVersion === `cta-and-recs` && this._isLoggedInPocketUser) {
|
||
this._getAndBuildPocketRecs();
|
||
this._enableRecShowHide();
|
||
} else if (ctaVersion === `cta-and-recs` && !this._isLoggedInPocketUser) {
|
||
// Fall back to cta only for logged out users:
|
||
ctaVersion = `cta-only`;
|
||
}
|
||
|
||
if (ctaVersion == `cta-only`) {
|
||
this._enableDismissCTA();
|
||
}
|
||
|
||
elPocketCTAWrapper.hidden = false;
|
||
elPocketCTAWrapper.classList.add(`pocket-cta-container-${ctaVersion}`);
|
||
elPocketCTAWrapper.classList.add(
|
||
`pocket-cta-container-${
|
||
this._isLoggedInPocketUser ? `logged-in` : `logged-out`
|
||
}`
|
||
);
|
||
|
||
// Set up tracking for sign up buttons
|
||
this._doc
|
||
.querySelectorAll(`.pocket-sign-up, .pocket-discover-more`)
|
||
.forEach(el => {
|
||
el.addEventListener(`click`, e => {
|
||
Services.telemetry.recordEvent(
|
||
"readermode",
|
||
"pocket_cta",
|
||
"sign_up_click",
|
||
null,
|
||
{}
|
||
);
|
||
});
|
||
});
|
||
|
||
// Set up tracking for user seeing CTA
|
||
this._ctaIntersectionObserver.observe(
|
||
this._doc.querySelector(`#pocket-cta-container`)
|
||
);
|
||
}
|
||
},
|
||
};
|