@readium/navigator-html-injectables 2.6.3 → 2.7.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.
@@ -1 +1 @@
1
- import{getCssSelector as i}from"@readium/shared";import{TextQuoteAnchor as l}from"../vendor/hypothesis/anchoring/types.js";function a(o){const t=o.tagName.toUpperCase();return t==="IMG"||t==="VIDEO"||t==="AUDIO"||t==="IFRAME"||t==="OBJECT"||t==="EMBED"||t==="CANVAS"}function g(o,t){try{const r=t.locations,f=t.text;if(f&&f.highlight){let e;r&&i(r)&&(e=o.querySelector(i(r))),e||(e=o.body);const n=new l(e,f.highlight,{prefix:f.before,suffix:f.after});try{return n.toRange()}catch{return console.warn("Quote not found:",n),null}}if(r){let e=null;if(!e&&i(r)&&(e=o.querySelector(i(r))),!e&&r.fragments){for(const n of r.fragments)if(e=o.getElementById(n),e)break}if(e){const n=o.createRange();return e.childNodes.length===0||a(e)?(n.selectNode(e),n):(n.setStartBefore(e),n.setEndAfter(e),n)}}}catch(r){console.error(r)}return null}export{g as rangeFromLocator};
1
+ import{getDomRange as u,getTextFragment as g,getCssSelector as a}from"@readium/shared";import{processTextFragmentDirective as d}from"@readium/helpers";import{TextQuoteAnchor as m}from"../vendor/hypothesis/anchoring/types.js";function h(n){const r=n.tagName.toUpperCase();return r==="IMG"||r==="VIDEO"||r==="AUDIO"||r==="IFRAME"||r==="OBJECT"||r==="EMBED"||r==="CANVAS"}function f(n,r){const o=r&&a(r);let l=null;if(o)try{l=n.querySelector(o)}catch(e){console.warn(`Invalid cssSelector: ${o}`,e)}return l??n.body??n.documentElement}function i(n,r){let o;try{o=n.querySelector(r.cssSelector)}catch(s){return console.error(`Invalid domRange cssSelector: ${r.cssSelector}`,s),null}if(!o)return console.error(`Can't resolve domRange cssSelector: ${r.cssSelector}`),null;const l=n.createTreeWalker(o,NodeFilter.SHOW_TEXT);let e=0,t;for(;t=l.nextNode();){if(e===r.textNodeIndex)return{node:t,offset:r.charOffset};e++}return console.error(`Can't resolve domRange textNodeIndex ${r.textNodeIndex} for selector: ${r.cssSelector}`),null}function E(n,r){try{const o=r.locations,l=r.text;if(o){const e=u(o);if(e){const t=i(n,e.start),s=e.end?i(n,e.end):t;if(t&&s)try{const c=n.createRange();return t.offset!==void 0?c.setStart(t.node,t.offset):c.setStartBefore(t.node),s.offset!==void 0?c.setEnd(s.node,s.offset):c.setEndBefore(s.node),c}catch(c){console.warn("Invalid domRange, falling back:",c)}}}if(o){const e=g(o);if(e){const t=f(n,o),s=d(e,n,t);if(s.length>0)return s[0]}}if(l&&l.highlight){const e=f(n,o),t=new m(e,l.highlight,{prefix:l.before,suffix:l.after});try{return t.toRange()}catch{return console.warn("Quote not found:",t),null}}if(o){let e=null;if(!e&&a(o)&&(e=n.querySelector(a(o))),!e&&o.fragments){for(const t of o.fragments)if(e=n.getElementById(t),e)break}if(e){const t=n.createRange();return e.childNodes.length===0||h(e)?(t.selectNode(e),t):(t.setStartBefore(e),t.setEndAfter(e),t)}}}catch(o){console.error(o)}return null}export{E as rangeFromLocator};
@@ -1 +1 @@
1
- import{Module as l}from"../Module.js";const i=class i extends l{constructor(){super(...arguments),this.mediaPlayingCount=0,this.allAnimations=new Set}wndOnErr(e){this.comms?.send("error",{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno})}unblock(e){for(e._readium_blockEvents=!1;e._readium_blockedEvents?.length>0;){const t=e._readium_blockedEvents.shift();switch(t[0]){case 0:Reflect.apply(t[1],t[2],t[3]);break;case 1:const a=t[1],n=t[2];e.removeEventListener(a.type,e._readium_eventBlocker,!0);const s=new Event(a.type,{bubbles:a.bubbles,cancelable:a.cancelable});n?n.dispatchEvent(s):e.dispatchEvent(s);break}}}onMediaPlayEvent(){this.mediaPlayingCount++,this.comms?.send("media_play",this.mediaPlayingCount)}onMediaPauseEvent(){this.mediaPlayingCount>0&&this.mediaPlayingCount--,this.comms?.send("media_pause",this.mediaPlayingCount)}pauseAllMedia(e){const t=e.document.querySelectorAll("audio,video");for(let a=0;a<t.length;a++)t[a].pause()}mount(e,t){this.comms=t,e.addEventListener("error",this.wndOnErr,!1),Reflect.defineProperty(e.navigator,"epubReadingSystem",{value:{name:"readium-ts-toolkit",version:"2.6.3",hasFeature:(n,s="")=>{switch(n){case"dom-manipulation":return!0;case"layout-changes":return!0;case"touch-events":return!0;case"mouse-events":return!0;case"keyboard-events":return!0;case"spine-scripting":return!0;case"embedded-web-content":return!0;default:return!1}}},writable:!1}),"getAnimations"in e.document&&e.document.getAnimations().forEach(n=>{n.cancel(),this.allAnimations.add(n)}),t.register("activate",i.moduleName,(n,s)=>{this.allAnimations.forEach(r=>{r.cancel(),r.play()}),s(!0)}),t.register("unfocus",i.moduleName,(n,s)=>{this.pauseAllMedia(e),this.allAnimations.forEach(r=>r.pause()),s(!0)});const a=e.document.querySelectorAll("audio,video");for(let n=0;n<a.length;n++){const s=a[n];s.addEventListener("play",this.onMediaPlayEvent,{passive:!0}),s.addEventListener("pause",this.onMediaPauseEvent,{passive:!0})}return t.log("Setup Mounted"),!0}unmount(e,t){return e.removeEventListener("error",this.wndOnErr),e.removeEventListener("play",this.onMediaPlayEvent),e.removeEventListener("pause",this.onMediaPauseEvent),this.allAnimations.forEach(a=>a.cancel()),this.allAnimations.clear(),t.log("Setup Unmounted"),!0}};i.moduleName="setup";let o=i;export{o as Setup};
1
+ import{Module as l}from"../Module.js";const i=class i extends l{constructor(){super(...arguments),this.mediaPlayingCount=0,this.allAnimations=new Set}wndOnErr(e){this.comms?.send("error",{message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno})}unblock(e){for(e._readium_blockEvents=!1;e._readium_blockedEvents?.length>0;){const t=e._readium_blockedEvents.shift();switch(t[0]){case 0:Reflect.apply(t[1],t[2],t[3]);break;case 1:const a=t[1],n=t[2];e.removeEventListener(a.type,e._readium_eventBlocker,!0);const s=new Event(a.type,{bubbles:a.bubbles,cancelable:a.cancelable});n?n.dispatchEvent(s):e.dispatchEvent(s);break}}}onMediaPlayEvent(){this.mediaPlayingCount++,this.comms?.send("media_play",this.mediaPlayingCount)}onMediaPauseEvent(){this.mediaPlayingCount>0&&this.mediaPlayingCount--,this.comms?.send("media_pause",this.mediaPlayingCount)}pauseAllMedia(e){const t=e.document.querySelectorAll("audio,video");for(let a=0;a<t.length;a++)t[a].pause()}mount(e,t){this.comms=t,e.addEventListener("error",this.wndOnErr,!1),Reflect.defineProperty(e.navigator,"epubReadingSystem",{value:{name:"readium-ts-toolkit",version:"2.7.0",hasFeature:(n,s="")=>{switch(n){case"dom-manipulation":return!0;case"layout-changes":return!0;case"touch-events":return!0;case"mouse-events":return!0;case"keyboard-events":return!0;case"spine-scripting":return!0;case"embedded-web-content":return!0;default:return!1}}},writable:!1}),"getAnimations"in e.document&&e.document.getAnimations().forEach(n=>{n.cancel(),this.allAnimations.add(n)}),t.register("activate",i.moduleName,(n,s)=>{this.allAnimations.forEach(r=>{r.cancel(),r.play()}),s(!0)}),t.register("unfocus",i.moduleName,(n,s)=>{this.pauseAllMedia(e),this.allAnimations.forEach(r=>r.pause()),s(!0)});const a=e.document.querySelectorAll("audio,video");for(let n=0;n<a.length;n++){const s=a[n];s.addEventListener("play",this.onMediaPlayEvent,{passive:!0}),s.addEventListener("pause",this.onMediaPauseEvent,{passive:!0})}return t.log("Setup Mounted"),!0}unmount(e,t){return e.removeEventListener("error",this.wndOnErr),e.removeEventListener("play",this.onMediaPlayEvent),e.removeEventListener("pause",this.onMediaPauseEvent),this.allAnimations.forEach(a=>a.cancel()),this.allAnimations.clear(),t.log("Setup Unmounted"),!0}};i.moduleName="setup";let o=i;export{o as Setup};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator-html-injectables",
3
- "version": "2.6.3",
3
+ "version": "2.7.0",
4
4
  "type": "module",
5
5
  "description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator",
6
6
  "author": "readium",
@@ -44,8 +44,8 @@
44
44
  "node": ">=18"
45
45
  },
46
46
  "dependencies": {
47
- "@readium/helpers": "1.0.0",
48
- "@readium/shared": "2.4.0"
47
+ "@readium/helpers": "1.1.0",
48
+ "@readium/shared": "2.5.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "css-selector-generator": "^3.8.0",
@@ -1,4 +1,5 @@
1
- import { getCssSelector, Locator } from "@readium/shared";
1
+ import { DomRangePoint, getCssSelector, getDomRange, getTextFragment, Locator, LocatorLocations } from "@readium/shared";
2
+ import { processTextFragmentDirective } from "@readium/helpers";
2
3
  import { TextQuoteAnchor } from "../vendor/hypothesis/anchoring/types.ts";
3
4
 
4
5
  function isReplacedLikeElement(element: Element): boolean {
@@ -6,19 +7,104 @@ function isReplacedLikeElement(element: Element): boolean {
6
7
  return tagName === "IMG" || tagName === "VIDEO" || tagName === "AUDIO" || tagName === "IFRAME" || tagName === "OBJECT" || tagName === "EMBED" || tagName === "CANVAS";
7
8
  }
8
9
 
10
+ // Root used for text-based searches: the cssSelector-referenced element when
11
+ // present and resolvable, otherwise the whole document body.
12
+ function resolveTextSearchRoot(doc: Document, locations: LocatorLocations | undefined): Element {
13
+ const cssSelector = locations && getCssSelector(locations);
14
+ let root: Element | null = null;
15
+ if (cssSelector) {
16
+ try {
17
+ root = doc.querySelector(cssSelector);
18
+ } catch (error) {
19
+ console.warn(`Invalid cssSelector: ${cssSelector}`, error);
20
+ }
21
+ }
22
+ return root ?? doc.body ?? doc.documentElement;
23
+ }
24
+
25
+ function resolveDomRangePoint(doc: Document, point: DomRangePoint): { node: Text; offset?: number } | null {
26
+ let container: Element | null;
27
+ try {
28
+ container = doc.querySelector(point.cssSelector);
29
+ } catch (error) {
30
+ console.error(`Invalid domRange cssSelector: ${point.cssSelector}`, error);
31
+ return null;
32
+ }
33
+ if (!container) {
34
+ console.error(`Can't resolve domRange cssSelector: ${point.cssSelector}`);
35
+ return null;
36
+ }
37
+
38
+ const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT);
39
+ let index = 0;
40
+ let node: Node | null;
41
+ while ((node = walker.nextNode())) {
42
+ if (index === point.textNodeIndex) {
43
+ return { node: node as Text, offset: point.charOffset };
44
+ }
45
+ index++;
46
+ }
47
+
48
+ console.error(`Can't resolve domRange textNodeIndex ${point.textNodeIndex} for selector: ${point.cssSelector}`);
49
+ return null;
50
+ }
51
+
9
52
  // Based on the kotlin-toolkit code
10
53
  export function rangeFromLocator(doc: Document, locator: Locator) {
11
54
  try {
12
55
  const locations = locator.locations;
13
56
  const text = locator.text;
14
- if (text && text.highlight) {
15
- let root;
16
- if (locations && getCssSelector(locations)) {
17
- root = doc.querySelector(getCssSelector(locations)!);
57
+
58
+ // Tried before text.highlight: a domRange pinpoints an exact node/offset,
59
+ // which disambiguates between multiple occurrences of the same quote that
60
+ // a text-based search alone cannot tell apart.
61
+ if (locations) {
62
+ const domRange = getDomRange(locations);
63
+ if (domRange) {
64
+ const start = resolveDomRangePoint(doc, domRange.start);
65
+ const end = domRange.end ? resolveDomRangePoint(doc, domRange.end) : start;
66
+ if (start && end) {
67
+ try {
68
+ const range = doc.createRange();
69
+
70
+ if (start.offset !== undefined) {
71
+ range.setStart(start.node, start.offset);
72
+ } else {
73
+ range.setStartBefore(start.node);
74
+ }
75
+
76
+ if (end.offset !== undefined) {
77
+ range.setEnd(end.node, end.offset);
78
+ } else {
79
+ range.setEndBefore(end.node);
80
+ }
81
+
82
+ return range;
83
+ } catch (error) {
84
+ console.warn("Invalid domRange, falling back:", error);
85
+ }
86
+ }
18
87
  }
19
- if (!root) {
20
- root = doc.body;
88
+ }
89
+
90
+ // Tried before text.highlight for the same reason as domRange: a
91
+ // ":~:text=" fragment is a strict superset of what text.highlight can
92
+ // express (it also supports a textStart...textEnd range), so checking
93
+ // text.highlight first would mean this branch is rarely reached.
94
+ if (locations) {
95
+ const fragmentDirective = getTextFragment(locations);
96
+
97
+ if (fragmentDirective) {
98
+ const root = resolveTextSearchRoot(doc, locations);
99
+ const results = processTextFragmentDirective(fragmentDirective, doc, root);
100
+ if (results.length > 0) {
101
+ return results[0]!;
102
+ }
21
103
  }
104
+ }
105
+
106
+ if (text && text.highlight) {
107
+ const root = resolveTextSearchRoot(doc, locations);
22
108
 
23
109
  const anchor = new TextQuoteAnchor(root, text.highlight, {
24
110
  prefix: text.before,