@readium/navigator-html-injectables 1.2.0
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.
- package/README.MD +13 -0
- package/dist/index.js +3144 -0
- package/dist/index.umd.cjs +69 -0
- package/package.json +58 -0
- package/src/Loader.ts +85 -0
- package/src/comms/comms.ts +168 -0
- package/src/comms/index.ts +3 -0
- package/src/comms/keys.ts +42 -0
- package/src/comms/mid.ts +5 -0
- package/src/helpers/animation.ts +5 -0
- package/src/helpers/css.ts +16 -0
- package/src/helpers/document.ts +43 -0
- package/src/helpers/dom.ts +126 -0
- package/src/helpers/locator.ts +67 -0
- package/src/helpers/rect.ts +301 -0
- package/src/helpers/scrollSnapperHelper.ts +43 -0
- package/src/index.ts +3 -0
- package/src/modules/Decorator.ts +462 -0
- package/src/modules/Module.ts +23 -0
- package/src/modules/ModuleLibrary.ts +44 -0
- package/src/modules/Peripherals.ts +148 -0
- package/src/modules/index.ts +3 -0
- package/src/modules/setup/FixedSetup.ts +73 -0
- package/src/modules/setup/ReflowableSetup.ts +65 -0
- package/src/modules/setup/Setup.ts +131 -0
- package/src/modules/snapper/ColumnSnapper.ts +468 -0
- package/src/modules/snapper/ScrollSnapper.ts +168 -0
- package/src/modules/snapper/Snapper.ts +49 -0
- package/src/vendor/approx-string-match/LICENSE +21 -0
- package/src/vendor/approx-string-match/README.MD +1 -0
- package/src/vendor/approx-string-match/index.ts +362 -0
- package/src/vendor/hypothesis/README.MD +1 -0
- package/src/vendor/hypothesis/anchoring/api-types.ts +309 -0
- package/src/vendor/hypothesis/anchoring/html.ts +134 -0
- package/src/vendor/hypothesis/anchoring/match-quote.ts +163 -0
- package/src/vendor/hypothesis/anchoring/placeholder.ts +59 -0
- package/src/vendor/hypothesis/anchoring/text-range.ts +327 -0
- package/src/vendor/hypothesis/anchoring/trim-range.ts +220 -0
- package/src/vendor/hypothesis/anchoring/types.ts +377 -0
- package/src/vendor/hypothesis/anchoring/xpath.ts +164 -0
- package/src/vendor/hypothesis/tsconfig.json +33 -0
- package/src/vendor/hypothesis/types/shared.ts +40 -0
- package/types/src/Loader.d.ts +33 -0
- package/types/src/comms/comms.d.ts +40 -0
- package/types/src/comms/index.d.ts +3 -0
- package/types/src/comms/keys.d.ts +2 -0
- package/types/src/comms/mid.d.ts +1 -0
- package/types/src/helpers/animation.d.ts +1 -0
- package/types/src/helpers/css.d.ts +4 -0
- package/types/src/helpers/document.d.ts +8 -0
- package/types/src/helpers/dom.d.ts +15 -0
- package/types/src/helpers/locator.d.ts +2 -0
- package/types/src/helpers/rect.d.ts +10 -0
- package/types/src/helpers/scrollSnapperHelper.d.ts +5 -0
- package/types/src/index.d.ts +3 -0
- package/types/src/modules/Decorator.d.ts +43 -0
- package/types/src/modules/Module.d.ts +11 -0
- package/types/src/modules/ModuleLibrary.d.ts +5 -0
- package/types/src/modules/Peripherals.d.ts +39 -0
- package/types/src/modules/ReflowablePeripherals.d.ts +37 -0
- package/types/src/modules/index.d.ts +3 -0
- package/types/src/modules/setup/FixedSetup.d.ts +9 -0
- package/types/src/modules/setup/ReflowableSetup.d.ts +9 -0
- package/types/src/modules/setup/Setup.d.ts +17 -0
- package/types/src/modules/snapper/ColumnSnapper.d.ts +41 -0
- package/types/src/modules/snapper/ScrollSnapper.d.ts +16 -0
- package/types/src/modules/snapper/Snapper.d.ts +11 -0
- package/types/src/vendor/approx-string-match/index.d.ts +54 -0
- package/types/src/vendor/hypothesis/anchoring/api-types.d.ts +266 -0
- package/types/src/vendor/hypothesis/anchoring/html.d.ts +17 -0
- package/types/src/vendor/hypothesis/anchoring/match-quote.d.ts +30 -0
- package/types/src/vendor/hypothesis/anchoring/placeholder.d.ts +32 -0
- package/types/src/vendor/hypothesis/anchoring/text-range.d.ts +103 -0
- package/types/src/vendor/hypothesis/anchoring/trim-range.d.ts +17 -0
- package/types/src/vendor/hypothesis/anchoring/types.d.ts +102 -0
- package/types/src/vendor/hypothesis/anchoring/xpath.d.ts +15 -0
- package/types/src/vendor/hypothesis/types/shared.d.ts +32 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ReadiumWindow } from "./dom";
|
|
2
|
+
|
|
3
|
+
export function isRTL(wnd: ReadiumWindow): boolean {
|
|
4
|
+
return wnd.document.body.dir.toLowerCase() === "rtl";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getColumnCountPerScreen(wnd: ReadiumWindow) {
|
|
8
|
+
return parseInt(
|
|
9
|
+
wnd.getComputedStyle(
|
|
10
|
+
wnd.document.documentElement
|
|
11
|
+
).getPropertyValue("column-count")
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
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";
|
|
37
|
+
virtualCol.style.breakBefore = "column";
|
|
38
|
+
virtualCol.innerHTML = "​"; // zero-width space
|
|
39
|
+
wnd.document.body.appendChild(virtualCol);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// (From swift-toolkit @ https://github.com/hypothesis/client/blob/main/src/annotator/highlighter.ts)
|
|
2
|
+
|
|
3
|
+
import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
|
|
4
|
+
import type { getCssSelector } from "css-selector-generator";
|
|
5
|
+
|
|
6
|
+
type BlockedEventData = [0, Function, any[], any[]] | [1, Event, EventTarget];
|
|
7
|
+
|
|
8
|
+
// This is what is injected into the HTML documents
|
|
9
|
+
export interface ReadiumWindow extends Window {
|
|
10
|
+
_readium_blockEvents: boolean;
|
|
11
|
+
_readium_blockedEvents: BlockedEventData[];
|
|
12
|
+
_readium_eventBlocker: EventListenerOrEventListenerObject;
|
|
13
|
+
_readium_cssSelectorGenerator: {
|
|
14
|
+
getCssSelector: typeof getCssSelector;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deselect(wnd: ReadiumWindow) {
|
|
19
|
+
const currentSelection = wnd.getSelection();
|
|
20
|
+
if(currentSelection) currentSelection.removeAllRanges();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const interactiveTags = [
|
|
24
|
+
"a",
|
|
25
|
+
"audio",
|
|
26
|
+
"button",
|
|
27
|
+
"canvas",
|
|
28
|
+
"details",
|
|
29
|
+
"input",
|
|
30
|
+
"label",
|
|
31
|
+
"option",
|
|
32
|
+
"select",
|
|
33
|
+
"submit",
|
|
34
|
+
"textarea",
|
|
35
|
+
"video",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// See https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling
|
|
39
|
+
export function nearestInteractiveElement(element: Element): Element | null {
|
|
40
|
+
if (interactiveTags.indexOf(element.nodeName.toLowerCase()) !== -1) {
|
|
41
|
+
return element;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Checks whether the element is editable by the user.
|
|
45
|
+
if (
|
|
46
|
+
element.hasAttribute("contenteditable") &&
|
|
47
|
+
element.getAttribute("contenteditable")?.toLowerCase() !== "false"
|
|
48
|
+
) {
|
|
49
|
+
return element;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Checks parents recursively because the touch might be for example on an <em> inside a <a>.
|
|
53
|
+
if (element.parentElement) {
|
|
54
|
+
return nearestInteractiveElement(element.parentElement);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Returns the `Locator` object to the first block element that is visible on
|
|
61
|
+
/// the screen.
|
|
62
|
+
export function findFirstVisibleLocator(wnd: ReadiumWindow, scrolling: boolean) {
|
|
63
|
+
const element = findElement(wnd, wnd.document.body, scrolling);
|
|
64
|
+
return new Locator({
|
|
65
|
+
href: "#",
|
|
66
|
+
type: "application/xhtml+xml",
|
|
67
|
+
locations: new LocatorLocations({
|
|
68
|
+
otherLocations: new Map([
|
|
69
|
+
["cssSelector", wnd._readium_cssSelectorGenerator.getCssSelector(element)]
|
|
70
|
+
])
|
|
71
|
+
}),
|
|
72
|
+
text: new LocatorText({
|
|
73
|
+
highlight: element.textContent || undefined
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findElement(wnd: ReadiumWindow, rootElement: Element, scrolling: boolean): Element {
|
|
79
|
+
for (var i = 0; i < rootElement.children.length; i++) {
|
|
80
|
+
const child = rootElement.children[i];
|
|
81
|
+
if (!shouldIgnoreElement(child) && isElementVisible(wnd, child, scrolling)) {
|
|
82
|
+
return findElement(wnd, child, scrolling);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return rootElement;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isElementVisible(wnd: ReadiumWindow, element: Element, scrolling: boolean) {
|
|
89
|
+
// if (readium.isFixedLayout) return true;
|
|
90
|
+
|
|
91
|
+
if (element === document.body || element === document.documentElement) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (!document || !document.documentElement || !document.body) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rect = element.getBoundingClientRect();
|
|
99
|
+
if (scrolling) {
|
|
100
|
+
return rect.bottom > 0 && rect.top < wnd.innerHeight;
|
|
101
|
+
} else {
|
|
102
|
+
return rect.right > 0 && rect.left < wnd.innerWidth;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function shouldIgnoreElement(element: Element) {
|
|
107
|
+
const elStyle = getComputedStyle(element);
|
|
108
|
+
if (elStyle) {
|
|
109
|
+
const display = elStyle.getPropertyValue("display");
|
|
110
|
+
if (display != "block") {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// Cannot be relied upon, because web browser engine reports invisible when out of view in
|
|
114
|
+
// scrolled columns!
|
|
115
|
+
// const visibility = elStyle.getPropertyValue("visibility");
|
|
116
|
+
// if (visibility === "hidden") {
|
|
117
|
+
// return false;
|
|
118
|
+
// }
|
|
119
|
+
const opacity = elStyle.getPropertyValue("opacity");
|
|
120
|
+
if (opacity === "0") {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Locator } from "@readium/shared";
|
|
2
|
+
import { TextQuoteAnchor } from "../vendor/hypothesis/anchoring/types";
|
|
3
|
+
|
|
4
|
+
// Based on the kotlin-toolkit code
|
|
5
|
+
export function rangeFromLocator(doc: Document, locator: Locator) {
|
|
6
|
+
try {
|
|
7
|
+
let locations = locator.locations;
|
|
8
|
+
let text = locator.text;
|
|
9
|
+
if (text && text.highlight) {
|
|
10
|
+
let root;
|
|
11
|
+
if (locations && locations.getCssSelector()) {
|
|
12
|
+
root = doc.querySelector(locations.getCssSelector()!);
|
|
13
|
+
}
|
|
14
|
+
if (!root) {
|
|
15
|
+
root = doc.body;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let anchor = new TextQuoteAnchor(root, text.highlight, {
|
|
19
|
+
prefix: text.before,
|
|
20
|
+
suffix: text.after,
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return anchor.toRange();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// We don't watch to "crash" when the quote is not found
|
|
26
|
+
console.warn("Quote not found:", anchor);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (locations) {
|
|
32
|
+
var element = null;
|
|
33
|
+
|
|
34
|
+
if (!element && locations.getCssSelector()) {
|
|
35
|
+
element = doc.querySelector(locations.getCssSelector()!);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!element && locations.fragments) {
|
|
39
|
+
for (const htmlId of locations.fragments) {
|
|
40
|
+
element = doc.getElementById(htmlId);
|
|
41
|
+
if (element) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (element) {
|
|
48
|
+
let range = doc.createRange();
|
|
49
|
+
|
|
50
|
+
// This is a special case where the node is
|
|
51
|
+
// a single element with no children. Not sure
|
|
52
|
+
// yet how effective this is yet, may remove in future.
|
|
53
|
+
if(element.childNodes.length === 0) {
|
|
54
|
+
range.selectNodeContents(element);
|
|
55
|
+
return range;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
range.setStartBefore(element);
|
|
59
|
+
range.setEndAfter(element);
|
|
60
|
+
return range;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(e);
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Mostly copied from the mobile toolkit and adjusted for typescript
|
|
2
|
+
// https://github.com/readium/swift-toolkit/blob/develop/Sources/Navigator/EPUB/Scripts/src/rect.js
|
|
3
|
+
|
|
4
|
+
export interface Rect {
|
|
5
|
+
bottom: number;
|
|
6
|
+
height: number;
|
|
7
|
+
left: number;
|
|
8
|
+
right: number;
|
|
9
|
+
top: number;
|
|
10
|
+
width: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getClientRectsNoOverlap(
|
|
14
|
+
range: Range,
|
|
15
|
+
doNotMergeHorizontallyAlignedRects: boolean
|
|
16
|
+
) {
|
|
17
|
+
let clientRects = range.getClientRects();
|
|
18
|
+
|
|
19
|
+
// Try falling back to the client rects of the common ancestor of the range if it's an HTML Element
|
|
20
|
+
if(!clientRects.length)
|
|
21
|
+
if(range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE)
|
|
22
|
+
clientRects = (range.commonAncestorContainer as HTMLElement).getClientRects();
|
|
23
|
+
|
|
24
|
+
const tolerance = 1;
|
|
25
|
+
const originalRects: Rect[] = [];
|
|
26
|
+
for (const rangeClientRect of clientRects) {
|
|
27
|
+
originalRects.push({
|
|
28
|
+
bottom: rangeClientRect.bottom,
|
|
29
|
+
height: rangeClientRect.height,
|
|
30
|
+
left: rangeClientRect.left,
|
|
31
|
+
right: rangeClientRect.right,
|
|
32
|
+
top: rangeClientRect.top,
|
|
33
|
+
width: rangeClientRect.width,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const mergedRects = mergeTouchingRects(
|
|
37
|
+
originalRects,
|
|
38
|
+
tolerance,
|
|
39
|
+
doNotMergeHorizontallyAlignedRects
|
|
40
|
+
);
|
|
41
|
+
const noContainedRects = removeContainedRects(mergedRects, tolerance);
|
|
42
|
+
const newRects = replaceOverlapingRects(noContainedRects);
|
|
43
|
+
const minArea = 2 * 2;
|
|
44
|
+
for (let j = newRects.length - 1; j >= 0; j--) {
|
|
45
|
+
const rect = newRects[j];
|
|
46
|
+
const bigEnough = rect.width * rect.height > minArea;
|
|
47
|
+
if (!bigEnough) {
|
|
48
|
+
if (newRects.length > 1) {
|
|
49
|
+
// log("CLIENT RECT: remove small");
|
|
50
|
+
newRects.splice(j, 1);
|
|
51
|
+
} else {
|
|
52
|
+
// log("CLIENT RECT: remove small, but keep otherwise empty!");
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// log(`CLIENT RECT: reduced ${originalRects.length} --> ${newRects.length}`);
|
|
58
|
+
return newRects;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergeTouchingRects(
|
|
62
|
+
rects: Rect[],
|
|
63
|
+
tolerance: number,
|
|
64
|
+
doNotMergeHorizontallyAlignedRects: boolean
|
|
65
|
+
): Rect[] {
|
|
66
|
+
for (let i = 0; i < rects.length; i++) {
|
|
67
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
68
|
+
const rect1 = rects[i];
|
|
69
|
+
const rect2 = rects[j];
|
|
70
|
+
if (rect1 === rect2) {
|
|
71
|
+
// log("mergeTouchingRects rect1 === rect2 ??!");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const rectsLineUpVertically =
|
|
75
|
+
almostEqual(rect1.top, rect2.top, tolerance) &&
|
|
76
|
+
almostEqual(rect1.bottom, rect2.bottom, tolerance);
|
|
77
|
+
const rectsLineUpHorizontally =
|
|
78
|
+
almostEqual(rect1.left, rect2.left, tolerance) &&
|
|
79
|
+
almostEqual(rect1.right, rect2.right, tolerance);
|
|
80
|
+
const horizontalAllowed = !doNotMergeHorizontallyAlignedRects;
|
|
81
|
+
const aligned =
|
|
82
|
+
(rectsLineUpHorizontally && horizontalAllowed) ||
|
|
83
|
+
(rectsLineUpVertically && !rectsLineUpHorizontally);
|
|
84
|
+
const canMerge = aligned && rectsTouchOrOverlap(rect1, rect2, tolerance);
|
|
85
|
+
if (canMerge) {
|
|
86
|
+
/*log(
|
|
87
|
+
`CLIENT RECT: merging two into one, VERTICAL: ${rectsLineUpVertically} HORIZONTAL: ${rectsLineUpHorizontally} (${doNotMergeHorizontallyAlignedRects})`
|
|
88
|
+
);*/
|
|
89
|
+
const newRects = rects.filter((rect) => {
|
|
90
|
+
return rect !== rect1 && rect !== rect2;
|
|
91
|
+
});
|
|
92
|
+
const replacementClientRect = getBoundingRect(rect1, rect2);
|
|
93
|
+
newRects.push(replacementClientRect);
|
|
94
|
+
return mergeTouchingRects(
|
|
95
|
+
newRects,
|
|
96
|
+
tolerance,
|
|
97
|
+
doNotMergeHorizontallyAlignedRects
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return rects;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getBoundingRect(rect1: Rect, rect2: Rect) {
|
|
106
|
+
const left = Math.min(rect1.left, rect2.left);
|
|
107
|
+
const right = Math.max(rect1.right, rect2.right);
|
|
108
|
+
const top = Math.min(rect1.top, rect2.top);
|
|
109
|
+
const bottom = Math.max(rect1.bottom, rect2.bottom);
|
|
110
|
+
return {
|
|
111
|
+
bottom,
|
|
112
|
+
height: bottom - top,
|
|
113
|
+
left,
|
|
114
|
+
right,
|
|
115
|
+
top,
|
|
116
|
+
width: right - left,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function removeContainedRects(rects: Rect[], tolerance: number) {
|
|
121
|
+
const rectsToKeep = new Set(rects);
|
|
122
|
+
for (const rect of rects) {
|
|
123
|
+
const bigEnough = rect.width > 1 && rect.height > 1;
|
|
124
|
+
if (!bigEnough) {
|
|
125
|
+
// log("CLIENT RECT: remove tiny");
|
|
126
|
+
rectsToKeep.delete(rect);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
for (const possiblyContainingRect of rects) {
|
|
130
|
+
if (rect === possiblyContainingRect) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!rectsToKeep.has(possiblyContainingRect)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (rectContains(possiblyContainingRect, rect, tolerance)) {
|
|
137
|
+
// log("CLIENT RECT: remove contained");
|
|
138
|
+
rectsToKeep.delete(rect);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Array.from(rectsToKeep);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rectContains(rect1: Rect, rect2: Rect, tolerance: number) {
|
|
147
|
+
return (
|
|
148
|
+
rectContainsPoint(rect1, rect2.left, rect2.top, tolerance) &&
|
|
149
|
+
rectContainsPoint(rect1, rect2.right, rect2.top, tolerance) &&
|
|
150
|
+
rectContainsPoint(rect1, rect2.left, rect2.bottom, tolerance) &&
|
|
151
|
+
rectContainsPoint(rect1, rect2.right, rect2.bottom, tolerance)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function rectContainsPoint(rect: Rect, x: number, y: number, tolerance: number) {
|
|
156
|
+
return (
|
|
157
|
+
(rect.left < x || almostEqual(rect.left, x, tolerance)) &&
|
|
158
|
+
(rect.right > x || almostEqual(rect.right, x, tolerance)) &&
|
|
159
|
+
(rect.top < y || almostEqual(rect.top, y, tolerance)) &&
|
|
160
|
+
(rect.bottom > y || almostEqual(rect.bottom, y, tolerance))
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function replaceOverlapingRects(rects: Rect[]): Rect[] {
|
|
165
|
+
for (let i = 0; i < rects.length; i++) {
|
|
166
|
+
for (let j = i + 1; j < rects.length; j++) {
|
|
167
|
+
const rect1 = rects[i];
|
|
168
|
+
const rect2 = rects[j];
|
|
169
|
+
if (rect1 === rect2) {
|
|
170
|
+
// log("replaceOverlapingRects rect1 === rect2 ??!");
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (rectsTouchOrOverlap(rect1, rect2, -1)) {
|
|
174
|
+
let toAdd = [];
|
|
175
|
+
let toRemove: Rect;
|
|
176
|
+
const subtractRects1 = rectSubtract(rect1, rect2);
|
|
177
|
+
if (subtractRects1.length === 1) {
|
|
178
|
+
toAdd = subtractRects1;
|
|
179
|
+
toRemove = rect1;
|
|
180
|
+
} else {
|
|
181
|
+
const subtractRects2 = rectSubtract(rect2, rect1);
|
|
182
|
+
if (subtractRects1.length < subtractRects2.length) {
|
|
183
|
+
toAdd = subtractRects1;
|
|
184
|
+
toRemove = rect1;
|
|
185
|
+
} else {
|
|
186
|
+
toAdd = subtractRects2;
|
|
187
|
+
toRemove = rect2;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// log(`CLIENT RECT: overlap, cut one rect into ${toAdd.length}`);
|
|
191
|
+
const newRects = rects.filter((rect) => {
|
|
192
|
+
return rect !== toRemove;
|
|
193
|
+
});
|
|
194
|
+
Array.prototype.push.apply(newRects, toAdd);
|
|
195
|
+
return replaceOverlapingRects(newRects);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return rects;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function rectSubtract(rect1: Rect, rect2: Rect) {
|
|
203
|
+
const rectIntersected = rectIntersect(rect2, rect1);
|
|
204
|
+
if (rectIntersected.height === 0 || rectIntersected.width === 0) {
|
|
205
|
+
return [rect1];
|
|
206
|
+
}
|
|
207
|
+
const rects = [];
|
|
208
|
+
{
|
|
209
|
+
const rectA = {
|
|
210
|
+
bottom: rect1.bottom,
|
|
211
|
+
height: 0,
|
|
212
|
+
left: rect1.left,
|
|
213
|
+
right: rectIntersected.left,
|
|
214
|
+
top: rect1.top,
|
|
215
|
+
width: 0,
|
|
216
|
+
};
|
|
217
|
+
rectA.width = rectA.right - rectA.left;
|
|
218
|
+
rectA.height = rectA.bottom - rectA.top;
|
|
219
|
+
if (rectA.height !== 0 && rectA.width !== 0) {
|
|
220
|
+
rects.push(rectA);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
{
|
|
224
|
+
const rectB = {
|
|
225
|
+
bottom: rectIntersected.top,
|
|
226
|
+
height: 0,
|
|
227
|
+
left: rectIntersected.left,
|
|
228
|
+
right: rectIntersected.right,
|
|
229
|
+
top: rect1.top,
|
|
230
|
+
width: 0,
|
|
231
|
+
};
|
|
232
|
+
rectB.width = rectB.right - rectB.left;
|
|
233
|
+
rectB.height = rectB.bottom - rectB.top;
|
|
234
|
+
if (rectB.height !== 0 && rectB.width !== 0) {
|
|
235
|
+
rects.push(rectB);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
{
|
|
239
|
+
const rectC = {
|
|
240
|
+
bottom: rect1.bottom,
|
|
241
|
+
height: 0,
|
|
242
|
+
left: rectIntersected.left,
|
|
243
|
+
right: rectIntersected.right,
|
|
244
|
+
top: rectIntersected.bottom,
|
|
245
|
+
width: 0,
|
|
246
|
+
};
|
|
247
|
+
rectC.width = rectC.right - rectC.left;
|
|
248
|
+
rectC.height = rectC.bottom - rectC.top;
|
|
249
|
+
if (rectC.height !== 0 && rectC.width !== 0) {
|
|
250
|
+
rects.push(rectC);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
{
|
|
254
|
+
const rectD = {
|
|
255
|
+
bottom: rect1.bottom,
|
|
256
|
+
height: 0,
|
|
257
|
+
left: rectIntersected.right,
|
|
258
|
+
right: rect1.right,
|
|
259
|
+
top: rect1.top,
|
|
260
|
+
width: 0,
|
|
261
|
+
};
|
|
262
|
+
rectD.width = rectD.right - rectD.left;
|
|
263
|
+
rectD.height = rectD.bottom - rectD.top;
|
|
264
|
+
if (rectD.height !== 0 && rectD.width !== 0) {
|
|
265
|
+
rects.push(rectD);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return rects;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function rectIntersect(rect1: Rect, rect2: Rect) {
|
|
272
|
+
const maxLeft = Math.max(rect1.left, rect2.left);
|
|
273
|
+
const minRight = Math.min(rect1.right, rect2.right);
|
|
274
|
+
const maxTop = Math.max(rect1.top, rect2.top);
|
|
275
|
+
const minBottom = Math.min(rect1.bottom, rect2.bottom);
|
|
276
|
+
return {
|
|
277
|
+
bottom: minBottom,
|
|
278
|
+
height: Math.max(0, minBottom - maxTop),
|
|
279
|
+
left: maxLeft,
|
|
280
|
+
right: minRight,
|
|
281
|
+
top: maxTop,
|
|
282
|
+
width: Math.max(0, minRight - maxLeft),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function rectsTouchOrOverlap(rect1: Rect, rect2: Rect, tolerance: number) {
|
|
287
|
+
return (
|
|
288
|
+
(rect1.left < rect2.right ||
|
|
289
|
+
(tolerance >= 0 && almostEqual(rect1.left, rect2.right, tolerance))) &&
|
|
290
|
+
(rect2.left < rect1.right ||
|
|
291
|
+
(tolerance >= 0 && almostEqual(rect2.left, rect1.right, tolerance))) &&
|
|
292
|
+
(rect1.top < rect2.bottom ||
|
|
293
|
+
(tolerance >= 0 && almostEqual(rect1.top, rect2.bottom, tolerance))) &&
|
|
294
|
+
(rect2.top < rect1.bottom ||
|
|
295
|
+
(tolerance >= 0 && almostEqual(rect2.top, rect1.bottom, tolerance)))
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function almostEqual(a: number, b: number, tolerance: number) {
|
|
300
|
+
return Math.abs(a - b) <= tolerance;
|
|
301
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
}
|
package/src/index.ts
ADDED