From 8bce04f907df2fe76e36dd5036a014c02ebd7e5c Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Mon, 27 Nov 2023 16:39:47 +0800 Subject: [PATCH 01/26] fix: data-viewer-theme attribute would leak at top level document this will use a #root element if exists instead --- src/model/user-settings/UserSettings.ts | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/model/user-settings/UserSettings.ts b/src/model/user-settings/UserSettings.ts index 4ff4301f..c9e6634e 100644 --- a/src/model/user-settings/UserSettings.ts +++ b/src/model/user-settings/UserSettings.ts @@ -413,8 +413,10 @@ export class UserSettings implements IUserSettings { "html" ) as HTMLHtmlElement; if (html) { - const rootElement = document.documentElement; - const body = HTMLUtilities.findRequiredElement(rootElement, "body"); + const rootElement = + HTMLUtilities.findElement(document, "#root") || + document.documentElement; + const body = HTMLUtilities.findElement(html, "body"); // // Apply publishers default // html.style.removeProperty(ReadiumCSS.PUBLISHER_DEFAULT_KEY); @@ -435,8 +437,9 @@ export class UserSettings implements IUserSettings { // Apply appearance html.style.removeProperty(ReadiumCSS.APPEARANCE_KEY); - HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); - HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); + if (rootElement) + HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); + if (body) HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); // Apply font family html.style.removeProperty(ReadiumCSS.FONT_FAMILY_KEY); @@ -468,8 +471,10 @@ export class UserSettings implements IUserSettings { ) as HTMLHtmlElement; if (html) { - const rootElement = document.documentElement; - const body = HTMLUtilities.findRequiredElement(rootElement, "body"); + const rootElement = + HTMLUtilities.findElement(document, "#root") || + document.documentElement; + const body = HTMLUtilities.findElement(html, "body"); if (this.view?.navigator.publication.isReflowable) { // Apply font size if (await this.getProperty(ReadiumCSS.FONT_SIZE_KEY)) { @@ -578,18 +583,21 @@ export class UserSettings implements IUserSettings { if ( this.userProperties.getByRef(ReadiumCSS.APPEARANCE_REF)?.value === 0 ) { - HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); - HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); + if (rootElement) + HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); + if (body) HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); } else if ( this.userProperties.getByRef(ReadiumCSS.APPEARANCE_REF)?.value === 1 ) { - HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "sepia"); - HTMLUtilities.setAttr(body, "data-viewer-theme", "sepia"); + if (rootElement) + HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "sepia"); + if (body) HTMLUtilities.setAttr(body, "data-viewer-theme", "sepia"); } else if ( this.userProperties.getByRef(ReadiumCSS.APPEARANCE_REF)?.value === 2 ) { - HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "night"); - HTMLUtilities.setAttr(body, "data-viewer-theme", "night"); + if (rootElement) + HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "night"); + if (body) HTMLUtilities.setAttr(body, "data-viewer-theme", "night"); } } else { html.style.setProperty( @@ -598,8 +606,9 @@ export class UserSettings implements IUserSettings { .getByRef(ReadiumCSS.APPEARANCE_REF) ?.toString() ?? null ); - HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); - HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); + if (rootElement) + HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); + if (body) HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); } if (this.view?.navigator.publication.isReflowable) { // Apply font family From 45e7964f54c5c84432e9291d1b87964878bceebb Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 13 Dec 2023 15:04:04 +0800 Subject: [PATCH 02/26] fix: navigator.handleIFrameLoad is now iframe aware before that the function would be called for each iframe, but often loop through the iframes, causing duplicate effects --- src/modules/AnnotationModule.ts | 4 +- src/modules/consumption/ConsumptionModule.ts | 5 +- src/modules/highlight/TextHighlighter.ts | 12 +- src/modules/linefocus/LineFocusModule.ts | 7 +- .../protection/ContentProtectionModule.ts | 50 +++--- src/navigator/IFrameNavigator.ts | 150 +++++++++--------- 6 files changed, 110 insertions(+), 118 deletions(-) diff --git a/src/modules/AnnotationModule.ts b/src/modules/AnnotationModule.ts index 3ed3da24..a7976939 100644 --- a/src/modules/AnnotationModule.ts +++ b/src/modules/AnnotationModule.ts @@ -197,7 +197,7 @@ export class AnnotationModule implements ReaderModule { }, 200); } - initialize() { + initialize(iframe: HTMLIFrameElement) { return new Promise(async (resolve) => { await (document as any).fonts.ready; if (this.rights.enableAnnotations) { @@ -205,7 +205,7 @@ export class AnnotationModule implements ReaderModule { this.drawHighlights(); this.showHighlights(); addEventListenerOptional( - this.navigator.iframes[0].contentDocument?.body, + iframe.contentDocument?.body, "click", this.click.bind(this) ); diff --git a/src/modules/consumption/ConsumptionModule.ts b/src/modules/consumption/ConsumptionModule.ts index f4e8f2f3..5a1fd94e 100644 --- a/src/modules/consumption/ConsumptionModule.ts +++ b/src/modules/consumption/ConsumptionModule.ts @@ -84,8 +84,9 @@ export class ConsumptionModule implements ReaderModule { log.log("Consumption module stop"); this.endResearchSession(); } - initialize() { - let win = this.navigator.iframes[0].contentWindow; + + initialize(iframe: HTMLIFrameElement) { + let win = iframe.contentWindow; if (win) { const self = this; win.onload = function () { diff --git a/src/modules/highlight/TextHighlighter.ts b/src/modules/highlight/TextHighlighter.ts index 3ca9aac7..3f2aa108 100644 --- a/src/modules/highlight/TextHighlighter.ts +++ b/src/modules/highlight/TextHighlighter.ts @@ -191,16 +191,12 @@ export class TextHighlighter { onAfterHighlight: function () {}, }); } - async initialize() { - let doc = this.navigator.iframes[0].contentDocument; + async initialize(iframe: HTMLIFrameElement) { + let doc = iframe.contentDocument; if (doc) { this.dom(doc.body).addClass(this.options.contextClass); } - this.bindEvents( - this.navigator.iframes[0].contentDocument?.body, - this, - this.hasEventListener - ); + this.bindEvents(iframe.contentDocument?.body, this, this.hasEventListener); this.initializeToolbox(); @@ -218,7 +214,7 @@ export class TextHighlighter { } } setTimeout(async () => { - let doc = this.navigator.iframes[0].contentDocument; + let doc = iframe.contentDocument; if (doc) { await doc.body?.addEventListener("click", unselect); } diff --git a/src/modules/linefocus/LineFocusModule.ts b/src/modules/linefocus/LineFocusModule.ts index a51539e5..611102c6 100644 --- a/src/modules/linefocus/LineFocusModule.ts +++ b/src/modules/linefocus/LineFocusModule.ts @@ -135,7 +135,8 @@ export default class LineFocusModule implements ReaderModule { } } } - initialize() { + + initialize(iframe: HTMLIFrameElement) { return new Promise(async (resolve) => { await (document as any).fonts.ready; if (!this.hasEventListener) { @@ -143,12 +144,12 @@ export default class LineFocusModule implements ReaderModule { addEventListenerOptional(document, "keydown", this.keydown.bind(this)); addEventListenerOptional(document, "keyup", this.keyup.bind(this)); addEventListenerOptional( - this.navigator.iframes[0].contentDocument, + iframe.contentDocument, "keydown", this.keydown.bind(this) ); addEventListenerOptional( - this.navigator.iframes[0].contentDocument, + iframe.contentDocument, "keyup", this.keyup.bind(this) ); diff --git a/src/modules/protection/ContentProtectionModule.ts b/src/modules/protection/ContentProtectionModule.ts index 1f387700..1b3090e2 100644 --- a/src/modules/protection/ContentProtectionModule.ts +++ b/src/modules/protection/ContentProtectionModule.ts @@ -897,36 +897,34 @@ export class ContentProtectionModule implements ReaderModule { } } - public async initialize() { + public async initialize(iframe: HTMLIFrameElement) { if (this.properties?.enableObfuscation) { return new Promise(async (resolve) => { await (document as any).fonts.ready; - for (const iframe of this.navigator.iframes) { - if (iframe.contentDocument) { - const body = HTMLUtilities.findRequiredIframeElement( - iframe.contentDocument, - "body" - ) as HTMLBodyElement; - this.observe(); - - setTimeout(() => { - this.rects = this.findRects(body); - this.rects.forEach((rect) => - this.toggleRect(rect, this.securityContainer, this.isHacked) - ); + if (iframe.contentDocument) { + const body = HTMLUtilities.findRequiredIframeElement( + iframe.contentDocument, + "body" + ) as HTMLBodyElement; + this.observe(); - this.setupEvents(); - if (!this.hasEventListener) { - this.hasEventListener = true; - addEventListenerOptional( - this.wrapper, - "scroll", - this.handleScroll.bind(this) - ); - } - resolve(); - }, 10); - } + setTimeout(() => { + this.rects = this.findRects(body); + this.rects.forEach((rect) => + this.toggleRect(rect, this.securityContainer, this.isHacked) + ); + + this.setupEvents(); + if (!this.hasEventListener) { + this.hasEventListener = true; + addEventListenerOptional( + this.wrapper, + "scroll", + this.handleScroll.bind(this) + ); + } + resolve(); + }, 10); } }); } diff --git a/src/navigator/IFrameNavigator.ts b/src/navigator/IFrameNavigator.ts index fc5baedc..8ec0ee3a 100644 --- a/src/navigator/IFrameNavigator.ts +++ b/src/navigator/IFrameNavigator.ts @@ -897,7 +897,7 @@ export class IFrameNavigator extends EventEmitter implements Navigator { addEventListenerOptional( iframe, "load", - this.handleIFrameLoad.bind(this) + this.handleIFrameLoad.bind(this, iframe) ); } @@ -1330,7 +1330,7 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } } - private async handleIFrameLoad(): Promise { + private async handleIFrameLoad(iframe: HTMLIFrameElement): Promise { if (this.errorMessage) this.errorMessage.style.display = "none"; this.showLoadingMessageAfterDelay(); try { @@ -1439,28 +1439,26 @@ export class IFrameNavigator extends EventEmitter implements Navigator { this.chapterTitle.innerHTML = "(Current Chapter)"; } - await this.injectInjectablesIntoIframeHead(); + await this.injectInjectablesIntoIframeHead(iframe); if (this.highlighter !== undefined) { - await this.highlighter.initialize(); + await this.highlighter.initialize(iframe); } - const body = this.iframes[0].contentDocument?.body; + const body = iframe.contentDocument?.body; // resize on toggle details let details = body?.querySelector("details"); if (details) { let self = this; details.addEventListener("toggle", async (_event) => { - await self.view?.setIframeHeight?.(this.iframes[0]); + await self.view?.setIframeHeight?.(iframe); }); } if (this.eventHandler) { - for (const iframe of this.iframes) { - this.eventHandler.setupEvents(iframe.contentDocument); - this.touchEventHandler.setupEvents(iframe.contentDocument); - this.keyboardEventHandler.setupEvents(iframe.contentDocument); - } + this.eventHandler.setupEvents(iframe.contentDocument); + this.touchEventHandler.setupEvents(iframe.contentDocument); + this.keyboardEventHandler.setupEvents(iframe.contentDocument); this.touchEventHandler.setupEvents(this.errorMessage); if (!this.didInitKeyboardEventHandler) { this.keyboardEventHandler.keydown(document); @@ -1469,21 +1467,21 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } if (this.view?.layout !== "fixed") { if (this.view?.isScrollMode()) { - this.iframes[0].height = "0"; - this.view?.setIframeHeight?.(this.iframes[0]); + iframe.height = "0"; + this.view?.setIframeHeight?.(iframe); } } if (this.rights.enableContentProtection && this.contentProtectionModule) { - await this.contentProtectionModule.initialize(); + await this.contentProtectionModule.initialize(iframe); } if (this.rights.enableConsumption && this.consumptionModule) { - await this.consumptionModule.initialize(); + await this.consumptionModule.initialize(iframe); } if (this.rights.enableAnnotations && this.annotationModule) { - await this.annotationModule.initialize(); + await this.annotationModule.initialize(iframe); } if (this.rights.enableBookmarks && this.bookmarkModule) { @@ -1491,11 +1489,11 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } if (this.rights.enableLineFocus && this.lineFocusModule) { - await this.lineFocusModule.initialize(); + await this.lineFocusModule.initialize(iframe); } if (this.rights.enableTTS && this.ttsModule) { - const body = this.iframes[0].contentDocument?.body; + const body = iframe.contentDocument?.body; const ttsModule = this.ttsModule as TTSModule2; await ttsModule.initialize(body); } @@ -1514,9 +1512,9 @@ export class IFrameNavigator extends EventEmitter implements Navigator { setTimeout(async () => { if (this.newElementId) { - const element = ( - this.iframes[0].contentDocument as any - ).getElementById(this.newElementId); + const element = (iframe.contentDocument as any).getElementById( + this.newElementId + ); this.view?.goToElement?.(element); this.newElementId = undefined; } else if ( @@ -1541,7 +1539,7 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } this.hideLoadingMessage(); - this.showIframeContents(); + this.showIframeContents(iframe); if ( this.rights.enableMediaOverlays && @@ -1568,7 +1566,9 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } } - private async injectInjectablesIntoIframeHead(): Promise { + private async injectInjectablesIntoIframeHead( + iframe: HTMLIFrameElement + ): Promise { // Inject Readium CSS into Iframe Head const injectablesToLoad: Promise[] = []; @@ -1592,60 +1592,58 @@ export class IFrameNavigator extends EventEmitter implements Navigator { injectablesToLoad.push(loadPromise); }; - for (const iframe of this.iframes) { - const head = iframe.contentDocument?.head; - if (head) { - const bases = iframe.contentDocument.getElementsByTagName("base"); - if (bases.length === 0) { - head.insertBefore( - IFrameNavigator.createBase(this.currentChapterLink.href), - head.firstChild - ); - } + const head = iframe.contentDocument?.head; + if (head) { + const bases = iframe.contentDocument.getElementsByTagName("base"); + if (bases.length === 0) { + head.insertBefore( + IFrameNavigator.createBase(this.currentChapterLink.href), + head.firstChild + ); + } - this.injectables?.forEach((injectable) => { - if (injectable.type === "style") { - if (injectable.fontFamily) { - // UserSettings.fontFamilyValues.push(injectable.fontFamily) - // this.settings.setupEvents() - // this.settings.addFont(injectable.fontFamily); - this.settings.initAddedFont(); - if (!injectable.systemFont && injectable.url) { - const link = IFrameNavigator.createCssLink(injectable.url); - head.appendChild(link); - addLoadingInjectable(link); - } - } else if (injectable.r2before && injectable.url) { - const link = IFrameNavigator.createCssLink(injectable.url); - head.insertBefore(link, head.firstChild); - addLoadingInjectable(link); - } else if (injectable.r2default && injectable.url) { - const link = IFrameNavigator.createCssLink(injectable.url); - head.insertBefore(link, head.childNodes[1]); - addLoadingInjectable(link); - } else if (injectable.r2after && injectable.url) { - if (injectable.appearance) { - // this.settings.addAppearance(injectable.appearance); - this.settings.initAddedAppearance(); - } - const link = IFrameNavigator.createCssLink(injectable.url); - head.appendChild(link); - addLoadingInjectable(link); - } else if (injectable.url) { + this.injectables?.forEach((injectable) => { + if (injectable.type === "style") { + if (injectable.fontFamily) { + // UserSettings.fontFamilyValues.push(injectable.fontFamily) + // this.settings.setupEvents() + // this.settings.addFont(injectable.fontFamily); + this.settings.initAddedFont(); + if (!injectable.systemFont && injectable.url) { const link = IFrameNavigator.createCssLink(injectable.url); head.appendChild(link); addLoadingInjectable(link); } - } else if (injectable.type === "script" && injectable.url) { - const script = IFrameNavigator.createJavascriptLink( - injectable.url, - injectable.async ?? false - ); - head.appendChild(script); - addLoadingInjectable(script); + } else if (injectable.r2before && injectable.url) { + const link = IFrameNavigator.createCssLink(injectable.url); + head.insertBefore(link, head.firstChild); + addLoadingInjectable(link); + } else if (injectable.r2default && injectable.url) { + const link = IFrameNavigator.createCssLink(injectable.url); + head.insertBefore(link, head.childNodes[1]); + addLoadingInjectable(link); + } else if (injectable.r2after && injectable.url) { + if (injectable.appearance) { + // this.settings.addAppearance(injectable.appearance); + this.settings.initAddedAppearance(); + } + const link = IFrameNavigator.createCssLink(injectable.url); + head.appendChild(link); + addLoadingInjectable(link); + } else if (injectable.url) { + const link = IFrameNavigator.createCssLink(injectable.url); + head.appendChild(link); + addLoadingInjectable(link); } - }); - } + } else if (injectable.type === "script" && injectable.url) { + const script = IFrameNavigator.createJavascriptLink( + injectable.url, + injectable.async ?? false + ); + head.appendChild(script); + addLoadingInjectable(script); + } + }); } if (injectablesToLoad.length === 0) { @@ -3124,16 +3122,14 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } }, 200); - private showIframeContents() { + private showIframeContents(iframe: HTMLIFrameElement) { this.isBeingStyled = false; // We set a timeOut so that settings can be applied when opacity is still 0 setTimeout(() => { if (!this.isBeingStyled) { - this.iframes.forEach((iframe) => { - iframe.style.opacity = "1"; - iframe.style.border = "none"; - iframe.style.overflow = "hidden"; - }); + iframe.style.opacity = "1"; + iframe.style.border = "none"; + iframe.style.overflow = "hidden"; } }, 150); } From 826319918a9442d52f58752837847f6c5b80bf8e Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 13 Dec 2023 15:04:40 +0800 Subject: [PATCH 03/26] fix: disable highlighter for fixed view this would cause clicks on image pages to show the toolbox popup --- src/navigator/IFrameNavigator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigator/IFrameNavigator.ts b/src/navigator/IFrameNavigator.ts index 8ec0ee3a..ad1c356d 100644 --- a/src/navigator/IFrameNavigator.ts +++ b/src/navigator/IFrameNavigator.ts @@ -1441,7 +1441,7 @@ export class IFrameNavigator extends EventEmitter implements Navigator { await this.injectInjectablesIntoIframeHead(iframe); - if (this.highlighter !== undefined) { + if (this.view?.layout !== "fixed" && this.highlighter !== undefined) { await this.highlighter.initialize(iframe); } const body = iframe.contentDocument?.body; From e295fdfa8253551d0db2f1c78c1869fbfe8c1d1b Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 13 Dec 2023 15:01:51 +0800 Subject: [PATCH 04/26] fix: iframe 2 would be loaded twice this caused some issues on Firefox --- src/navigator/IFrameNavigator.ts | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/navigator/IFrameNavigator.ts b/src/navigator/IFrameNavigator.ts index ad1c356d..fafa3e9a 100644 --- a/src/navigator/IFrameNavigator.ts +++ b/src/navigator/IFrameNavigator.ts @@ -2008,24 +2008,24 @@ export class IFrameNavigator extends EventEmitter implements Navigator { } } else { this.iframes[0].src = "about:blank"; - } - if (this.iframes.length === 2) { - this.currentSpreadLinks.right = { - href: this.currentChapterLink.href, - }; + if (this.iframes.length === 2) { + this.currentSpreadLinks.right = { + href: this.currentChapterLink.href, + }; - if (isSameOrigin) { - this.iframes[1].src = this.currentChapterLink.href; - } else { - fetch(this.currentChapterLink.href, this.requestConfig) - .then((r) => r.text()) - .then(async (content) => { - writeIframe2Doc.call( - this, - content, - this.currentChapterLink.href - ); - }); + if (isSameOrigin) { + this.iframes[1].src = this.currentChapterLink.href; + } else { + fetch(this.currentChapterLink.href, this.requestConfig) + .then((r) => r.text()) + .then(async (content) => { + writeIframe2Doc.call( + this, + content, + this.currentChapterLink.href + ); + }); + } } } } From d799f2627a88435e3599e783ae178d023736686e Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Wed, 13 Dec 2023 15:00:57 +0800 Subject: [PATCH 05/26] feat: add support for reading direction --- src/model/user-settings/ReadiumCSS.ts | 2 + src/model/user-settings/UserSettings.ts | 60 ++++++++++++++++++++++++ src/navigator/IFrameNavigator.ts | 27 +++++++++++ src/utils/KeyboardEventHandler.ts | 39 +++++++++------- viewer/index_dita.html | 61 +++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 17 deletions(-) diff --git a/src/model/user-settings/ReadiumCSS.ts b/src/model/user-settings/ReadiumCSS.ts index d66fb34b..15e2a3e2 100644 --- a/src/model/user-settings/ReadiumCSS.ts +++ b/src/model/user-settings/ReadiumCSS.ts @@ -26,6 +26,7 @@ export class ReadiumCSS { // static readonly PUBLISHER_DEFAULT_REF = "advancedSettings"; static readonly TEXT_ALIGNMENT_REF = "textAlign"; static readonly COLUMN_COUNT_REF = "colCount"; + static readonly DIRECTION_REF = "direction"; static readonly WORD_SPACING_REF = "wordSpacing"; static readonly LETTER_SPACING_REF = "letterSpacing"; static readonly PAGE_MARGINS_REF = "pageMargins"; @@ -41,6 +42,7 @@ export class ReadiumCSS { static readonly TEXT_ALIGNMENT_KEY = "--USER__" + ReadiumCSS.TEXT_ALIGNMENT_REF; static readonly COLUMN_COUNT_KEY = "--USER__" + ReadiumCSS.COLUMN_COUNT_REF; + static readonly DIRECTION_KEY = "--USER__" + ReadiumCSS.DIRECTION_REF; static readonly WORD_SPACING_KEY = "--USER__" + ReadiumCSS.WORD_SPACING_REF; static readonly LETTER_SPACING_KEY = "--USER__" + ReadiumCSS.LETTER_SPACING_REF; diff --git a/src/model/user-settings/UserSettings.ts b/src/model/user-settings/UserSettings.ts index ab786223..05cd7cd8 100644 --- a/src/model/user-settings/UserSettings.ts +++ b/src/model/user-settings/UserSettings.ts @@ -73,6 +73,7 @@ export interface IUserSettings { // publisherDefaults: boolean; textAlignment: number; columnCount: number; + direction: number; wordSpacing: number; letterSpacing: number; pageMargins: number; @@ -100,6 +101,7 @@ export interface InitialUserSettings { // publisherDefaults?: boolean | "readium-advanced-on" | "readium-advanced-off"; textAlignment: number; columnCount: number; + direction: string; wordSpacing: number; letterSpacing: number; pageMargins: number; @@ -130,6 +132,7 @@ export class UserSettings implements IUserSettings { private static fontFamilyValues = ["Original", "serif", "sans-serif"]; private static readonly textAlignmentValues = ["auto", "justify", "start"]; private static readonly columnCountValues = ["auto", "1", "2"]; + private static readonly directionValues = ["auto", "ltr", "rtl"]; fontSize = 100.0; fontOverride = false; @@ -141,6 +144,7 @@ export class UserSettings implements IUserSettings { // publisherDefaults = true; textAlignment = 0; columnCount = 0; + direction = 0; wordSpacing = 0.0; letterSpacing = 0.0; pageMargins = 2.0; @@ -246,6 +250,17 @@ export class UserSettings implements IUserSettings { } log.log(settings.columnCount); } + if (initialUserSettings.direction) { + settings.direction = UserSettings.directionValues.findIndex( + (el: any) => el === initialUserSettings.direction + ); + let prop = settings.userProperties.getByRef(ReadiumCSS.DIRECTION_REF); + if (prop) { + prop.value = settings.direction; + await settings.saveProperty(prop); + } + log.log(settings.direction); + } if (initialUserSettings.wordSpacing) { settings.wordSpacing = initialUserSettings.wordSpacing; let prop = settings.userProperties.getByRef( @@ -361,6 +376,10 @@ export class UserSettings implements IUserSettings { "columnCount", ReadiumCSS.COLUMN_COUNT_KEY ); + this.direction = await this.getPropertyAndFallback( + "columnCount", + ReadiumCSS.DIRECTION_KEY + ); this.fontSize = await this.getPropertyAndFallback( "fontSize", @@ -397,6 +416,7 @@ export class UserSettings implements IUserSettings { // this.publisherDefaults = true; this.textAlignment = 0; this.columnCount = 0; + this.direction = 0; this.wordSpacing = 0.0; this.letterSpacing = 0.0; this.pageMargins = 2.0; @@ -424,6 +444,8 @@ export class UserSettings implements IUserSettings { html.style.removeProperty(ReadiumCSS.LETTER_SPACING_KEY); // Apply column count html.style.removeProperty(ReadiumCSS.COLUMN_COUNT_KEY); + // Apply direction + html.style.removeProperty(ReadiumCSS.DIRECTION_KEY); // Apply text alignment html.style.removeProperty(ReadiumCSS.TEXT_ALIGNMENT_KEY); // Apply line height @@ -599,6 +621,18 @@ export class UserSettings implements IUserSettings { HTMLUtilities.setAttr(rootElement, "data-viewer-theme", "day"); HTMLUtilities.setAttr(body, "data-viewer-theme", "day"); } + + if (this.view?.navigator.publication.isFixedLayout) { + if (await this.getProperty(ReadiumCSS.DIRECTION_KEY)) { + let value = + this.userProperties + .getByRef(ReadiumCSS.DIRECTION_REF) + ?.toString() ?? null; + html.style.setProperty(ReadiumCSS.DIRECTION_KEY, value); + this.view.navigator.setDirection(value); + } + } + if (this.view?.navigator.publication.isReflowable) { // Apply font family if (await this.getProperty(ReadiumCSS.FONT_FAMILY_KEY)) { @@ -784,6 +818,10 @@ export class UserSettings implements IUserSettings { await this.userProperties?.getByRef(ReadiumCSS.COLUMN_COUNT_REF) ?.value ], + direction: + UserSettings.directionValues[ + await this.userProperties?.getByRef(ReadiumCSS.DIRECTION_REF)?.value + ], wordSpacing: this.userProperties?.getByRef(ReadiumCSS.WORD_SPACING_REF) ?.value, letterSpacing: this.userProperties?.getByRef( @@ -827,6 +865,13 @@ export class UserSettings implements IUserSettings { ReadiumCSS.COLUMN_COUNT_REF, ReadiumCSS.COLUMN_COUNT_KEY ); + // Direction + userProperties.addEnumerable( + this.direction, + UserSettings.directionValues, + ReadiumCSS.DIRECTION_REF, + ReadiumCSS.DIRECTION_KEY + ); // Appearance userProperties.addEnumerable( this.appearance, @@ -976,6 +1021,10 @@ export class UserSettings implements IUserSettings { UserSettings.columnCountValues[ this.userProperties?.getByRef(ReadiumCSS.COLUMN_COUNT_REF)?.value ], // "auto", "1", "2" + direction: + UserSettings.directionValues[ + this.userProperties?.getByRef(ReadiumCSS.DIRECTION_REF)?.value + ], // "auto", "ltr", "rtl" verticalScroll: this.verticalScroll, fontSize: this.fontSize, wordSpacing: this.wordSpacing, @@ -1045,6 +1094,17 @@ export class UserSettings implements IUserSettings { this.settingsColumnsChangeCallback(); } + if (userSettings.direction) { + this.direction = UserSettings.directionValues.findIndex( + (el: any) => el === userSettings.direction + ); + let prop = this.userProperties?.getByRef(ReadiumCSS.DIRECTION_REF); + if (prop) { + prop.value = this.direction; + await this.storeProperty(prop); + } + } + if (userSettings.textAlignment) { this.textAlignment = UserSettings.textAlignmentValues.findIndex( (el: any) => el === userSettings.textAlignment diff --git a/src/navigator/IFrameNavigator.ts b/src/navigator/IFrameNavigator.ts index b7eb106c..93c83b81 100644 --- a/src/navigator/IFrameNavigator.ts +++ b/src/navigator/IFrameNavigator.ts @@ -117,6 +117,7 @@ export interface NavigatorAPI { resourceAtEnd: any; resourceFitsScreen: any; updateCurrentLocation: any; + direction: any; onError?: (e: Error) => void; } @@ -530,6 +531,19 @@ export class IFrameNavigator extends EventEmitter implements Navigator { spreads: HTMLDivElement; firstSpread: HTMLDivElement; + setDirection(direction?: string | null) { + let dir = ""; + if (direction === "rtl" || direction === "ltr") dir = direction; + if (direction === "auto") dir = this.publication.Metadata.Direction2; + if (dir) { + if (dir === "rtl") this.spreads.style.flexDirection = "row-reverse"; + if (dir === "ltr") this.spreads.style.flexDirection = "row"; + this.keyboardEventHandler.rtl = dir === "rtl"; + if (this.api?.direction) this.api?.direction(dir); + this.emit("direction", dir); + } + } + protected async start( mainElement: HTMLElement, headerMenu?: HTMLElement | null, @@ -574,6 +588,19 @@ export class IFrameNavigator extends EventEmitter implements Navigator { this.spreads.appendChild(this.firstSpread); this.firstSpread.appendChild(this.iframes[0]); wrapper.appendChild(this.spreads); + let dir = ""; + switch (this.settings.direction) { + case 0: + dir = "auto"; + break; + case 1: + dir = "ltr"; + break; + case 2: + dir = "rtl"; + break; + } + this.setDirection(dir); } else { iframe.setAttribute("height", "100%"); iframe.setAttribute("width", "100%"); diff --git a/src/utils/KeyboardEventHandler.ts b/src/utils/KeyboardEventHandler.ts index 76dcaab8..5e01bd92 100644 --- a/src/utils/KeyboardEventHandler.ts +++ b/src/utils/KeyboardEventHandler.ts @@ -21,8 +21,9 @@ import { IFrameNavigator } from "../navigator/IFrameNavigator"; export default class KeyboardEventHandler { navigator: IFrameNavigator; - constructor(navigator: IFrameNavigator) { +rtl: boolean; constructor(navigator: IFrameNavigator) { this.navigator = navigator; + this.rtl = false; } public onBackwardSwipe: (event: UIEvent) => void = () => {}; @@ -90,22 +91,26 @@ export default class KeyboardEventHandler { const key = event.key; switch (key) { case "ArrowRight": - self.onForwardSwipe(event); - break; - case "ArrowLeft": - self.onBackwardSwipe(event); - break; - } - switch (event.code) { - case "Space": - if (event.ctrlKey) { - self.onBackwardSwipe(event); - } else { - self.onForwardSwipe(event); - } - break; - } - }) + self.rtl + ? self.onBackwardSwipe(event) + : self.onForwardSwipe(event); + break; + case "ArrowLeft": + self.rtl + ? self.onForwardSwipe(event) + : self.onBackwardSwipe(event); + break; + } + switch (event.code) { + case "Space": + if (event.ctrlKey) { + self.onBackwardSwipe(event); + } else { + self.onForwardSwipe(event); + } + break; + } + }) ); } } diff --git a/viewer/index_dita.html b/viewer/index_dita.html index 63849861..247f2d0f 100644 --- a/viewer/index_dita.html +++ b/viewer/index_dita.html @@ -333,6 +333,44 @@ +
+ +
+ +
+