@readium/navigator-html-injectables 1.3.3 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,42 @@
1
1
  import { ReadiumWindow } from "./dom";
2
2
 
3
+ export function getProperties(wnd: ReadiumWindow) {
4
+ const cssProperties: { [key: string]: string } = {};
5
+
6
+ const rootStyle = wnd.document.documentElement.style;
7
+
8
+ for (const prop in wnd.document.documentElement.style) {
9
+ // We check if the property belongs to the CSSStyleDeclaration instance
10
+ // We also ensure that the property is a numeric index (indicating an inline style)
11
+ if (
12
+ Object.hasOwn(rootStyle, prop) &&
13
+ !Number.isNaN(Number.parseInt(prop))
14
+ ) {
15
+ cssProperties[rootStyle[prop]] = rootStyle.getPropertyValue(rootStyle[prop]);
16
+ }
17
+ }
18
+
19
+ return cssProperties;
20
+ }
21
+
22
+ export function updateProperties(wnd: ReadiumWindow, properties: { [key: string]: string }) {
23
+ const currentProperties = getProperties(wnd);
24
+
25
+ // Remove properties
26
+ Object.keys(currentProperties).forEach((key) => {
27
+ if (!properties.hasOwnProperty(key)) {
28
+ removeProperty(wnd, key);
29
+ }
30
+ });
31
+
32
+ // Update properties
33
+ Object.entries(properties).forEach(([key, value]) => {
34
+ if (currentProperties[key] !== value) {
35
+ setProperty(wnd, key, value);
36
+ }
37
+ });
38
+ }
39
+
3
40
  // Easy way to get a CSS property
4
41
  export function getProperty(wnd: ReadiumWindow, key: string) {
5
42
  return wnd.document.documentElement.style.getPropertyValue(key);
@@ -12,32 +12,53 @@ export function getColumnCountPerScreen(wnd: ReadiumWindow) {
12
12
  );
13
13
  }
14
14
 
15
+ /**
16
+ * Returns the "content height" of an element, which is its clientHeight
17
+ * minus any vertical padding.
18
+ *
19
+ * @param el - The element to measure.
20
+ */
21
+ export function getContentHeight(el: Element) {
22
+ const cStyle = getComputedStyle(el);
23
+ const paddingTop = parseFloat(cStyle.paddingTop || "0");
24
+ const paddingBottom = parseFloat(cStyle.paddingBottom || "0");
25
+ return el.clientHeight - paddingTop - paddingBottom;
26
+ }
27
+
15
28
  /**
16
- * Having an odd number of columns when displaying two columns per screen causes snapping and page
17
- * turning issues. To fix this, we insert a blank virtual column at the end of the resource.
29
+ * We have to make sure that the total number of columns is a multiple
30
+ * of the number of columns per screen.
31
+ * Otherwise it causes snapping and page turning issues.
32
+ * To fix this, we insert and remove blank virtual columns at the end of the resource.
18
33
  */
19
34
  export function appendVirtualColumnIfNeeded(wnd: ReadiumWindow) {
20
- const id = "readium-virtual-page";
21
- let virtualCol = wnd.document.getElementById(id);
22
- if (getColumnCountPerScreen(wnd) !== 2) {
23
- if (virtualCol) {
24
- virtualCol.remove();
25
- }
26
- } else {
27
- const documentWidth = wnd.document.scrollingElement!.scrollWidth;
28
- const colCount = documentWidth / wnd.innerWidth;
29
- const hasOddColCount = (Math.round(colCount * 2) / 2) % 1 > 0.1;
30
- if (hasOddColCount) {
31
- if (virtualCol)
32
- virtualCol.remove();
33
- else {
34
- virtualCol = wnd.document.createElement("div");
35
- virtualCol.setAttribute("id", id);
36
- virtualCol.dataset.readium = "true";
35
+ const virtualCols = wnd.document.querySelectorAll("div[id^='readium-virtual-page']");
36
+ // Remove first so that we don’t end up with an incorrect scrollWidth
37
+ // Even when removing their width we risk having an incorrect scrollWidth
38
+ // so removing them entirely is the most robust solution
39
+ for (const virtualCol of virtualCols) {
40
+ virtualCol.remove();
41
+ }
42
+ const colCount = getColumnCountPerScreen(wnd);
43
+ const documentWidth = wnd.document.scrollingElement!.scrollWidth;
44
+ const neededColCount = Math.ceil(documentWidth / wnd.innerWidth) * colCount;
45
+ const totalColCount = Math.round((documentWidth / wnd.innerWidth) * colCount);
46
+ const needed = colCount === 1 ? 0 : neededColCount - totalColCount;
47
+ if (needed > 0) {
48
+ for (let i = 0; i < needed; i++) {
49
+ const virtualCol = wnd.document.createElement("div");
50
+ virtualCol.setAttribute("id", `readium-virtual-page-${ i }`);
51
+ virtualCol.dataset.readium = "true";
52
+ if (CSS.supports("break-before", "column")) {
37
53
  virtualCol.style.breakBefore = "column";
38
- virtualCol.innerHTML = "&#8203;"; // zero-width space
39
- wnd.document.body.appendChild(virtualCol);
54
+ } else if (CSS.supports("break-inside", "avoid-column")) {
55
+ virtualCol.style.breakInside = "avoid-column";
56
+ virtualCol.style.height = getContentHeight(wnd.document.documentElement) + "px";
57
+ } else {
58
+ virtualCol.style.height = getContentHeight(wnd.document.documentElement) + "px";
40
59
  }
60
+ virtualCol.innerHTML = "&#8203;"; // zero-width space
61
+ wnd.document.body.appendChild(virtualCol);
41
62
  }
42
63
  }
43
64
  }
@@ -4,12 +4,9 @@ import { Module } from "./Module";
4
4
  import { rangeFromLocator } from "../helpers/locator";
5
5
  import { ModuleName } from "./ModuleLibrary";
6
6
  import { Rect, getClientRectsNoOverlap } from "../helpers/rect";
7
- import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
8
7
  import { getProperty } from "../helpers/css";
9
8
  import { ReadiumWindow } from "../helpers/dom";
10
-
11
- // Necessary for iOS 13 and below
12
- const ResizeObserver = window.ResizeObserver || Polyfill;
9
+ import { isDarkColor } from "../helpers/color";
13
10
 
14
11
  export enum Width {
15
12
  Wrap = "wrap", // Smallest width fitting the CSS border box.
@@ -254,7 +251,8 @@ class DecorationGroup {
254
251
  // template.innerHTML = item.decoration.element.trim();
255
252
  // TODO more styles logic
256
253
 
257
- const isDarkMode = getProperty(this.wnd, "--USER__appearance") === "readium-night-on";
254
+ const isDarkMode = getProperty(this.wnd, "--USER__appearance") === "readium-night-on" ||
255
+ isDarkColor(getProperty(this.wnd, "--USER__backgroundColor"));
258
256
 
259
257
  template.innerHTML = `
260
258
  <div
@@ -1,6 +1,6 @@
1
1
  import { Comms } from "../../comms/comms";
2
2
  import { Setup } from "./Setup";
3
- import { removeProperty, setProperty } from "../../helpers/css";
3
+ import { getProperties, removeProperty, setProperty, updateProperties } from "../../helpers/css";
4
4
  import { ReadiumWindow } from "../../helpers/dom";
5
5
 
6
6
  const VIEWPORT_META_TAG_ID = "readium-viewport";
@@ -35,6 +35,17 @@ export class ReflowableSetup extends Setup {
35
35
  target: wnd
36
36
  } as any); // Cheat!
37
37
 
38
+ comms.register("get_properties", ReflowableSetup.moduleName, (_, ack) => {
39
+ getProperties(wnd);
40
+ ack(true);
41
+ });
42
+ comms.register("update_properties", ReflowableSetup.moduleName, (data, ack) => {
43
+ // In order to keep the viewport width that is added in the setup itself,
44
+ // and don’t have EpubNavigator concerned with this, we need to add it there
45
+ (data as { [key: string]: string })["--RS__viewportWidth"] = `${ wnd.innerWidth }px`;
46
+ updateProperties(wnd, data as { [key: string]: string });
47
+ ack(true);
48
+ })
38
49
  comms.register("set_property", ReflowableSetup.moduleName, (data, ack) => {
39
50
  const kv = data as string[];
40
51
  setProperty(wnd, kv[0], kv[1]);
@@ -1,4 +1,3 @@
1
- import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
2
1
  import { Comms } from "../../comms";
3
2
  import { Snapper } from "./Snapper";
4
3
  import { getColumnCountPerScreen, isRTL, appendVirtualColumnIfNeeded } from "../../helpers/document";
@@ -272,18 +271,43 @@ export class ColumnSnapper extends Snapper {
272
271
  `;
273
272
  wnd.document.head.appendChild(d);
274
273
 
275
- // Necessary for iOS 13 and below
276
- const ResizeObserver = (wnd as ReadiumWindow & typeof globalThis).ResizeObserver || Polyfill;
277
-
278
- this.resizeObserver = new ResizeObserver(() => wnd.requestAnimationFrame(() => {
279
- wnd && appendVirtualColumnIfNeeded(wnd);
280
- }));
274
+ this.resizeObserver = new ResizeObserver(() => {
275
+ wnd.requestAnimationFrame(() => {
276
+ wnd && appendVirtualColumnIfNeeded(wnd);
277
+ });
278
+ this.onWidthChange();
279
+ });
281
280
  this.resizeObserver.observe(wnd.document.body);
282
- this.mutationObserver = new MutationObserver(() => {
283
- this.wnd.requestAnimationFrame(() => this.cachedScrollWidth = this.doc().scrollWidth!);
281
+
282
+ this.mutationObserver = new MutationObserver((mutations) => {
283
+ for (const mutation of mutations) {
284
+ // We have to check it’s not onTouchMove + snapOffset setting transforms
285
+ if (mutation.target === this.wnd.document.documentElement) {
286
+ const oldValue = mutation.oldValue as string;
287
+ const newValue = (mutation.target as HTMLElement).getAttribute("style") as string;
288
+ const transformRegex = /transform\s*:\s*([^;]+)/;
289
+ const oldValueTransform = oldValue?.match(transformRegex);
290
+ const newValueTransform = newValue?.match(transformRegex);
291
+ if (
292
+ (!oldValueTransform && !newValueTransform) ||
293
+ (oldValueTransform && !newValueTransform) ||
294
+ (oldValueTransform && newValueTransform && oldValueTransform[1] !== newValueTransform[1])
295
+ ) {
296
+ wnd.requestAnimationFrame(() => {
297
+ wnd && appendVirtualColumnIfNeeded(wnd);
298
+ });
299
+ this.onWidthChange();
300
+ }
301
+ } else {
302
+ wnd.requestAnimationFrame(() => this.cachedScrollWidth = this.doc().scrollWidth!);
303
+ }
304
+ }
284
305
  });
285
306
  wnd.frameElement && this.mutationObserver.observe(wnd.frameElement, {attributes: true, attributeFilter: ["style"]});
286
307
  this.mutationObserver.observe(wnd.document, {attributes: true, attributeFilter: ["style"]});
308
+ // For cases the resizeObserver is not able to detect cos body is not resizing despite colCount,
309
+ // we need to check the syle attribute on the documentElement (ReadiumCSS props)
310
+ this.mutationObserver.observe(wnd.document.documentElement, {attributes: true, attributeFilter: ["style"]});
287
311
 
288
312
  const scrollToOffset = (offset: number): boolean => {
289
313
  const oldScrollLeft = this.doc().scrollLeft;
@@ -387,6 +411,7 @@ export class ColumnSnapper extends Snapper {
387
411
 
388
412
  comms.register("go_prev", ColumnSnapper.moduleName, (_, ack) => {
389
413
  this.wnd.requestAnimationFrame(() => {
414
+ this.cachedScrollWidth = this.doc().scrollWidth!;
390
415
  const offset = wnd.scrollX - wnd.innerWidth;
391
416
  const minOffset = isRTL(wnd) ? - (this.cachedScrollWidth - wnd.innerWidth) : 0;
392
417
  const change = scrollToOffset(Math.max(offset, minOffset));
@@ -400,6 +425,7 @@ export class ColumnSnapper extends Snapper {
400
425
 
401
426
  comms.register("go_next", ColumnSnapper.moduleName, (_, ack) => {
402
427
  this.wnd.requestAnimationFrame(() => {
428
+ this.cachedScrollWidth = this.doc().scrollWidth!;
403
429
  const offset = wnd.scrollX + wnd.innerWidth;
404
430
  const maxOffset = isRTL(wnd) ? 0 : this.cachedScrollWidth - wnd.innerWidth;
405
431
  const change = scrollToOffset(Math.min(offset, maxOffset));
@@ -1,7 +1,6 @@
1
1
  import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
2
2
  import { Comms } from "../../comms";
3
3
  import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
4
- import { AnchorObserver, helperCreateAnchorElements, helperRemoveAnchorElements } from '../../helpers/scrollSnapperHelper';
5
4
  import { ModuleName } from "../ModuleLibrary";
6
5
  import { Snapper } from "./Snapper";
7
6
  import { rangeFromLocator } from "../../helpers/locator";
@@ -12,28 +11,28 @@ export class ScrollSnapper extends Snapper {
12
11
  static readonly moduleName: ModuleName = "scroll_snapper";
13
12
  private wnd!: ReadiumWindow;
14
13
  private comms!: Comms;
14
+ private resizeObserver!: ResizeObserver;
15
+ private isScrolling = false;
15
16
 
16
17
  private doc() {
17
18
  return this.wnd.document.scrollingElement as HTMLElement;
18
19
  }
19
20
 
20
- private createAnchorElements = () => {
21
- helperCreateAnchorElements(this.doc());
22
- }
23
-
24
- private removeAnchorElements = () => {
25
- helperRemoveAnchorElements(this.doc());
26
- }
27
-
28
- private createCustomElement = () => {
29
- customElements.get("anchor-observer") ||
30
- customElements.define("anchor-observer", AnchorObserver);
31
- }
32
-
33
21
  private reportProgress(data: { progress: number, reference: number }) {
34
22
  this.comms.send("progress", data);
35
23
  }
36
24
 
25
+ private handleScroll = () => {
26
+ if (!this.isScrolling) {
27
+ this.isScrolling = true;
28
+ this.wnd.requestAnimationFrame(() => {
29
+ const progress = this.doc().scrollTop / this.doc().offsetHeight;
30
+ this.reportProgress({ progress: progress, reference: this.wnd.innerHeight / this.doc().scrollHeight });
31
+ this.isScrolling = false;
32
+ });
33
+ }
34
+ };
35
+
37
36
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
38
37
  this.wnd = wnd;
39
38
  this.comms = comms;
@@ -55,6 +54,15 @@ export class ScrollSnapper extends Snapper {
55
54
  `;
56
55
  wnd.document.head.appendChild(style);
57
56
 
57
+ this.resizeObserver = new ResizeObserver(() => {
58
+ this.comms.ready && this.handleScroll();
59
+ });
60
+ this.resizeObserver.observe(wnd.document.body);
61
+
62
+ wnd.addEventListener("scroll", this.handleScroll, {
63
+ passive: true
64
+ });
65
+
58
66
  comms.register("go_progression", ScrollSnapper.moduleName, (data, ack) => {
59
67
  const position = data as number;
60
68
 
@@ -158,14 +166,13 @@ export class ScrollSnapper extends Snapper {
158
166
  });
159
167
 
160
168
  comms.log("ScrollSnapper Mounted");
161
- this.createCustomElement();
162
- this.createAnchorElements();
163
169
  return true;
164
170
  }
165
171
 
166
172
  unmount(wnd: ReadiumWindow, comms: Comms): boolean {
167
173
  comms.unregisterAll(ScrollSnapper.moduleName);
168
- this.removeAnchorElements();
174
+ this.resizeObserver.disconnect();
175
+ if (this.handleScroll) wnd.removeEventListener("scroll", this.handleScroll);
169
176
  wnd.document.getElementById(SCROLL_SNAPPER_STYLE_ID)?.remove();
170
177
  comms.log("ScrollSnapper Unmounted");
171
178
  return true;
@@ -1,2 +1,2 @@
1
1
  export type CommsEventKey = "_pong" | "_unhandled" | "_ack" | "log" | "error" | "click" | "tap" | "tap_more" | "no_more" | "no_less" | "swipe" | "progress" | "first_visible_locator" | "text_selected" | "media_play" | "media_pause";
2
- export type CommsCommandKey = "_ping" | "go_prev" | "go_next" | "go_id" | "go_text" | "go_end" | "go_start" | "go_progression" | "set_property" | "remove_property" | "first_visible_locator" | "decorate" | "protect" | "unprotect" | "unfocus" | "focus" | "activate" | "shake";
2
+ export type CommsCommandKey = "_ping" | "go_prev" | "go_next" | "go_id" | "go_text" | "go_end" | "go_start" | "go_progression" | "get_properties" | "update_properties" | "set_property" | "remove_property" | "first_visible_locator" | "decorate" | "protect" | "unprotect" | "unfocus" | "focus" | "activate" | "shake";
@@ -0,0 +1,16 @@
1
+ export declare const colorToRgba: (color: string) => {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ a: number;
6
+ };
7
+ export declare const getLuminance: (color: {
8
+ r: number;
9
+ g: number;
10
+ b: number;
11
+ a: number;
12
+ }) => number;
13
+ export declare const isDarkColor: (color: string) => boolean;
14
+ export declare const isLightColor: (color: string) => boolean;
15
+ export declare const checkContrast: (color1: string, color2: string) => number;
16
+ export declare const ensureContrast: (color1: string, color2: string, contrast?: number) => string[];
@@ -1,4 +1,10 @@
1
1
  import { ReadiumWindow } from "./dom";
2
+ export declare function getProperties(wnd: ReadiumWindow): {
3
+ [key: string]: string;
4
+ };
5
+ export declare function updateProperties(wnd: ReadiumWindow, properties: {
6
+ [key: string]: string;
7
+ }): void;
2
8
  export declare function getProperty(wnd: ReadiumWindow, key: string): string;
3
9
  export declare function setProperty(wnd: ReadiumWindow, key: string, value: string): void;
4
10
  export declare function removeProperty(wnd: ReadiumWindow, key: string): void;
@@ -2,7 +2,16 @@ import { ReadiumWindow } from "./dom";
2
2
  export declare function isRTL(wnd: ReadiumWindow): boolean;
3
3
  export declare function getColumnCountPerScreen(wnd: ReadiumWindow): number;
4
4
  /**
5
- * Having an odd number of columns when displaying two columns per screen causes snapping and page
6
- * turning issues. To fix this, we insert a blank virtual column at the end of the resource.
5
+ * Returns the "content height" of an element, which is its clientHeight
6
+ * minus any vertical padding.
7
+ *
8
+ * @param el - The element to measure.
9
+ */
10
+ export declare function getContentHeight(el: Element): number;
11
+ /**
12
+ * We have to make sure that the total number of columns is a multiple
13
+ * of the number of columns per screen.
14
+ * Otherwise it causes snapping and page turning issues.
15
+ * To fix this, we insert and remove blank virtual columns at the end of the resource.
7
16
  */
8
17
  export declare function appendVirtualColumnIfNeeded(wnd: ReadiumWindow): void;
@@ -6,11 +6,11 @@ export declare class ScrollSnapper extends Snapper {
6
6
  static readonly moduleName: ModuleName;
7
7
  private wnd;
8
8
  private comms;
9
+ private resizeObserver;
10
+ private isScrolling;
9
11
  private doc;
10
- private createAnchorElements;
11
- private removeAnchorElements;
12
- private createCustomElement;
13
12
  private reportProgress;
13
+ private handleScroll;
14
14
  mount(wnd: ReadiumWindow, comms: Comms): boolean;
15
15
  unmount(wnd: ReadiumWindow, comms: Comms): boolean;
16
16
  }
@@ -1,43 +0,0 @@
1
- const iframeAnchors = {
2
- top: "js-iframe-reader-top-anchor",
3
- bottom: "js-iframe-reader-bottom-anchor",
4
- };
5
-
6
- export class AnchorObserver extends HTMLElement {
7
- connectedCallback() {
8
- this.setAttribute("aria-hidden", "true");
9
- this.style.setProperty("display", "block", "important");
10
- this.style.setProperty("padding", "0", "important");
11
- this.style.setProperty("margin", "0", "important");
12
- this.style.setProperty("visibility", "hidden", "important");
13
- }
14
- }
15
-
16
- export function helperCreateAnchorElements(iframeElement: HTMLElement) {
17
- const body = iframeElement.getElementsByTagName("body")[0];
18
- const anchorTop = document.createElement("anchor-observer");
19
- const anchorBottom = document.createElement("anchor-observer");
20
- anchorTop.dataset.readium = "true";
21
- anchorBottom.dataset.readium = "true";
22
-
23
- anchorTop?.setAttribute("id", iframeAnchors.top);
24
- anchorTop?.style.setProperty("height", "115px", "important");
25
-
26
- anchorBottom?.setAttribute("id", iframeAnchors.bottom);
27
- anchorBottom?.style.setProperty("height", "80px", "important");
28
-
29
- body?.insertBefore(anchorTop, body.firstChild);
30
- body?.appendChild(anchorBottom);
31
- }
32
-
33
- export function helperRemoveAnchorElements(iframeElement: HTMLElement) {
34
- const body = iframeElement.getElementsByTagName("body")[0];
35
- const anchorTop = body?.querySelector(`#${iframeAnchors.top}`);
36
- const anchorBottom = body?.querySelector(`#${iframeAnchors.bottom}`);
37
- if (anchorTop) {
38
- anchorTop.parentElement?.removeChild(anchorTop);
39
- }
40
- if (anchorBottom) {
41
- anchorBottom.parentElement?.removeChild(anchorBottom);
42
- }
43
- }