@papyrus-sdk/ui-react-native 0.2.35-beta.8 → 0.2.35-beta.9
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/LICENSE +21 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1798 -1459
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1593 -1261
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/runtime/epubAnnotations.js +118 -0
- package/runtime/epubAnnotations.test.ts +31 -0
- package/runtime/epubSelectionMenu.test.ts +39 -0
- package/runtime/index.html +248 -11
- package/runtime/runtime.js +248 -11
- package/runtime/syncComicRuntime.mjs +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papyrus-sdk/ui-react-native",
|
|
3
|
-
"version": "0.2.35-beta.
|
|
3
|
+
"version": "0.2.35-beta.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"papyrus",
|
|
@@ -38,17 +38,13 @@
|
|
|
38
38
|
"bugs": {
|
|
39
39
|
"url": "https://github.com/solrachix/Papyrus/issues"
|
|
40
40
|
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "node runtime/syncComicRuntime.mjs && tsup",
|
|
43
|
-
"check:ink-worklets": "pnpm run build && node ../../scripts/ci/check-native-ink-worklets.cjs"
|
|
44
|
-
},
|
|
45
41
|
"dependencies": {
|
|
46
42
|
"react-native-svg": "^15.5.0"
|
|
47
43
|
},
|
|
48
44
|
"peerDependencies": {
|
|
49
|
-
"@papyrus-sdk/core": "
|
|
50
|
-
"@papyrus-sdk/engine-native": "
|
|
51
|
-
"@papyrus-sdk/types": "
|
|
45
|
+
"@papyrus-sdk/core": "0.2.23-beta.3",
|
|
46
|
+
"@papyrus-sdk/engine-native": "0.2.22-beta.7",
|
|
47
|
+
"@papyrus-sdk/types": "0.2.16-beta.3",
|
|
52
48
|
"@react-native-clipboard/clipboard": "^1.16.3",
|
|
53
49
|
"react": ">=18.2.0 <20",
|
|
54
50
|
"react-native": ">=0.81.0",
|
|
@@ -63,15 +59,19 @@
|
|
|
63
59
|
}
|
|
64
60
|
},
|
|
65
61
|
"devDependencies": {
|
|
66
|
-
"@papyrus-sdk/core": "0.2.23-beta.2",
|
|
67
|
-
"@papyrus-sdk/engine-native": "0.2.22-beta.6",
|
|
68
|
-
"@papyrus-sdk/types": "0.2.16-beta.2",
|
|
69
62
|
"@react-native-clipboard/clipboard": "^1.16.3",
|
|
70
63
|
"@types/react": "19.2.14",
|
|
71
64
|
"react": "^19.1.0",
|
|
72
65
|
"react-native": "^0.81.0",
|
|
73
66
|
"react-native-gesture-handler": "~2.28.0",
|
|
74
67
|
"react-native-safe-area-context": "^5.6.2",
|
|
75
|
-
"react-native-reanimated": "3.19.5"
|
|
68
|
+
"react-native-reanimated": "3.19.5",
|
|
69
|
+
"@papyrus-sdk/types": "0.2.16-beta.3",
|
|
70
|
+
"@papyrus-sdk/core": "0.2.23-beta.3",
|
|
71
|
+
"@papyrus-sdk/engine-native": "0.2.22-beta.7"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "node runtime/syncComicRuntime.mjs && tsup",
|
|
75
|
+
"check:ink-worklets": "pnpm run build && node ../../scripts/ci/check-native-ink-worklets.cjs"
|
|
76
76
|
}
|
|
77
|
-
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
(function (root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
3
|
+
else root.PapyrusEpubAnnotations = factory();
|
|
4
|
+
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
|
5
|
+
function validAnchor(anchor) {
|
|
6
|
+
return anchor && anchor.version === 1 && anchor.kind === 'epub-cfi' &&
|
|
7
|
+
typeof anchor.cfiRange === 'string' && anchor.cfiRange.length <= 4096 && /^epubcfi\([^\n<>]+\)$/.test(anchor.cfiRange) &&
|
|
8
|
+
typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
|
|
9
|
+
typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
|
|
10
|
+
}
|
|
11
|
+
function textNodes(document) {
|
|
12
|
+
var walker = document.createTreeWalker(document.body, 4);
|
|
13
|
+
var nodes = [], text = '', node;
|
|
14
|
+
while ((node = walker.nextNode())) {
|
|
15
|
+
if (node.parentElement.closest('script,style,[data-papyrus-overlay]')) continue;
|
|
16
|
+
nodes.push({node:node, start:text.length}); text += node.textContent;
|
|
17
|
+
}
|
|
18
|
+
return {nodes:nodes,text:text};
|
|
19
|
+
}
|
|
20
|
+
function recoverRange(document, anchor) {
|
|
21
|
+
var source = textNodes(document), match = null, start = source.text.indexOf(anchor.quote);
|
|
22
|
+
while (start >= 0) {
|
|
23
|
+
var end = start + anchor.quote.length;
|
|
24
|
+
if ((!anchor.prefix || source.text.slice(Math.max(0,start-anchor.prefix.length),start) === anchor.prefix) &&
|
|
25
|
+
(!anchor.suffix || source.text.slice(end,end+anchor.suffix.length) === anchor.suffix)) {
|
|
26
|
+
if (match) return null; match = {start:start,end:end};
|
|
27
|
+
}
|
|
28
|
+
start = source.text.indexOf(anchor.quote,start+1);
|
|
29
|
+
}
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
var range = document.createRange();
|
|
32
|
+
var first = source.nodes.find(function(n){return n.start+n.node.length > match.start;});
|
|
33
|
+
var last = source.nodes.find(function(n){return n.start+n.node.length >= match.end;});
|
|
34
|
+
if (!first || !last) return null;
|
|
35
|
+
range.setStart(first.node,match.start-first.start); range.setEnd(last.node,match.end-last.start);
|
|
36
|
+
return range;
|
|
37
|
+
}
|
|
38
|
+
function resolveRange(contents, anchor) {
|
|
39
|
+
if (!validAnchor(anchor)) return null;
|
|
40
|
+
try { var range = contents.range(anchor.cfiRange); if (range && range.toString() === anchor.quote) return range; } catch (_) {}
|
|
41
|
+
return recoverRange(contents.document,anchor);
|
|
42
|
+
}
|
|
43
|
+
function selection(contents, cfiRange, href, documentId) {
|
|
44
|
+
var range;
|
|
45
|
+
try { range = contents.range(cfiRange); } catch (_) { return null; }
|
|
46
|
+
if (!range || !range.toString()) return null;
|
|
47
|
+
var before = contents.document.createRange(); before.selectNodeContents(contents.document.body); before.setEnd(range.startContainer,range.startOffset);
|
|
48
|
+
var after = contents.document.createRange(); after.selectNodeContents(contents.document.body); after.setStart(range.endContainer,range.endOffset);
|
|
49
|
+
var anchor = {version:1,kind:'epub-cfi',cfiRange:cfiRange,href:href,spineIndex:contents.sectionIndex,documentId:documentId,quote:range.toString(),prefix:before.toString().slice(-48),suffix:after.toString().slice(0,48)};
|
|
50
|
+
return validAnchor(anchor) ? anchor : null;
|
|
51
|
+
}
|
|
52
|
+
function install(contents, options) {
|
|
53
|
+
var doc = contents.document, win = contents.window, alive = true, frame = 0;
|
|
54
|
+
var overlay = doc.createElement('div'); overlay.dataset.papyrusOverlay = 'annotations';
|
|
55
|
+
overlay.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;z-index:3';
|
|
56
|
+
var originalPosition=doc.body.style.position;
|
|
57
|
+
if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
|
|
58
|
+
doc.body.appendChild(overlay);
|
|
59
|
+
var registry = new Map(), hitRegions = [];
|
|
60
|
+
function render() {
|
|
61
|
+
frame = 0; if (!alive || !options.isCurrent()) return;
|
|
62
|
+
var keep = new Set(), noteGroups = new Map();
|
|
63
|
+
hitRegions = [];
|
|
64
|
+
options.annotations().forEach(function(annotation) {
|
|
65
|
+
var anchor = annotation.anchor;
|
|
66
|
+
if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
|
|
67
|
+
var range = resolveRange(contents,anchor); if (!range) return;
|
|
68
|
+
var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
|
|
69
|
+
if (!rects.length) return;
|
|
70
|
+
keep.add(annotation.id);
|
|
71
|
+
var group = registry.get(annotation.id);
|
|
72
|
+
if (!group) { group = doc.createElement('div'); group.dataset.annotationId = annotation.id; overlay.appendChild(group); registry.set(annotation.id,group); if (!doc.defaultView.matchMedia || !doc.defaultView.matchMedia('(prefers-reduced-motion: reduce)').matches) { if (typeof group.animate === 'function') group.animate([{opacity:0},{opacity:1}],{duration:180,easing:'ease-out'}); } }
|
|
73
|
+
group.replaceChildren();
|
|
74
|
+
hitRegions.push({id:annotation.id,rects:rects});
|
|
75
|
+
var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
|
|
76
|
+
var style = annotation.markupStyle || annotation.type;
|
|
77
|
+
var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
|
|
78
|
+
rects.forEach(function(rect) {
|
|
79
|
+
var mark = doc.createElement('div');
|
|
80
|
+
var bodyRect=doc.body.getBoundingClientRect();
|
|
81
|
+
var height = style === 'highlight' ? rect.height : 2;
|
|
82
|
+
var y = style === 'strikeout' ? rect.top+rect.height/2 : style === 'underline' ? rect.bottom-2 : rect.top;
|
|
83
|
+
mark.style.cssText = 'position:absolute;pointer-events:none;left:'+(rect.left-bodyRect.left)+'px;top:'+(y-bodyRect.top)+'px;width:'+rect.width+'px;height:'+height+'px;background:'+color+';opacity:'+opacity;
|
|
84
|
+
if (['highlight','underline','strikeout'].includes(style)) group.appendChild(mark);
|
|
85
|
+
});
|
|
86
|
+
var hasNote = annotation.noteContent !== undefined || annotation.type === 'comment' || annotation.type === 'text';
|
|
87
|
+
if (hasNote) {
|
|
88
|
+
var last = rects[rects.length-1], button = doc.createElement('button');
|
|
89
|
+
button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
|
|
90
|
+
button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+Math.max(0,Math.min(doc.documentElement.clientWidth-44,last.right-doc.body.getBoundingClientRect().left))+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
|
|
91
|
+
var key = Math.floor(last.top/44);
|
|
92
|
+
var neighbors=noteGroups.get(key);
|
|
93
|
+
if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
|
|
94
|
+
neighbors.ids.push(annotation.id);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
noteGroups.forEach(function(group){
|
|
98
|
+
group.button.textContent=group.ids.length>1?String(group.ids.length):'●';
|
|
99
|
+
group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
|
|
100
|
+
});
|
|
101
|
+
registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
|
|
102
|
+
}
|
|
103
|
+
function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
|
|
104
|
+
function tapMarkup(event) {
|
|
105
|
+
if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
|
|
106
|
+
var hit=hitRegions.find(function(item){return item.rects.some(function(r){return event.clientX>=r.left && event.clientX<=r.right && event.clientY>=r.top && event.clientY<=r.bottom;});});
|
|
107
|
+
if(hit) {event.stopPropagation();options.onTap(hit.id);}
|
|
108
|
+
}
|
|
109
|
+
doc.addEventListener('click',tapMarkup);
|
|
110
|
+
var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
|
|
111
|
+
if (observer) observer.observe(doc.body);
|
|
112
|
+
win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
|
|
113
|
+
if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
|
|
114
|
+
schedule();
|
|
115
|
+
return {schedule:schedule, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
|
|
116
|
+
}
|
|
117
|
+
return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
|
|
118
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {createRequire} from 'node:module';
|
|
2
|
+
import {describe,it,expect} from 'vitest';
|
|
3
|
+
const annotations = createRequire(import.meta.url)('./epubAnnotations.js');
|
|
4
|
+
const anchor = {version:1,kind:'epub-cfi',cfiRange:'epubcfi(/6/2!/4/2:0)',href:'chapter.xhtml',quote:'word'};
|
|
5
|
+
describe('EPUB contextual anchors',()=>{
|
|
6
|
+
it('rejects malformed CFI and unsafe hrefs',()=>{expect(annotations.validAnchor(anchor)).toBe(true);expect(annotations.validAnchor({...anchor,cfiRange:'bad'})).toBe(false);expect(annotations.validAnchor({...anchor,href:'javascript:alert(1)'})).toBe(false);});
|
|
7
|
+
it('recovers uniquely in chapter text preserving UTF16 and excludes overlays',()=>{document.body.innerHTML='<p>A 😀 word end</p><div data-papyrus-overlay>word</div>';expect(annotations.recoverRange(document,anchor)?.toString()).toBe('word');});
|
|
8
|
+
it('leaves ambiguous quotes unresolved unless context distinguishes them',()=>{document.body.innerHTML='<p>one word two word end</p>';expect(annotations.recoverRange(document,anchor)).toBeNull();expect(annotations.recoverRange(document,{...anchor,prefix:'two '})?.toString()).toBe('word');});
|
|
9
|
+
it('verifies exact CFI quote before controlled recovery',()=>{document.body.innerHTML='<p>other word</p>';const bad=document.createRange();bad.selectNodeContents(document.body);expect(annotations.resolveRange({document,range:()=>bad},anchor)?.toString()).toBe('word');});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders true strikeout, grouped notes and reconciles 120 stable IDs after reflow',()=>{
|
|
13
|
+
document.body.innerHTML='<p>word</p>';
|
|
14
|
+
const range=document.createRange();range.selectNodeContents(document.querySelector('p')!);
|
|
15
|
+
let top=20;Object.defineProperty(range,'getClientRects',{value:()=>[{left:10,right:50,top,bottom:top+20,width:40,height:20}]});
|
|
16
|
+
let queue:FrameRequestCallback[]=[];
|
|
17
|
+
const originalRAF=window.requestAnimationFrame;window.requestAnimationFrame=cb=>{queue.push(cb);return queue.length;};
|
|
18
|
+
let snapshot=Array.from({length:120},(_,i)=>({id:`n${i}`,anchor,color:'#ff0000',markupStyle:'strikeout',noteContent:`note ${i}`}));
|
|
19
|
+
const taps:string[]=[];
|
|
20
|
+
const renderer=annotations.install({document,window,range:()=>range},{href:'chapter.xhtml',documentId:undefined,annotations:()=>snapshot,labels:()=>({comment:'Nota'}),isCurrent:()=>true,onTap:(id:string)=>taps.push(id)});
|
|
21
|
+
const flush=()=>{const tasks=queue;queue=[];tasks.forEach(cb=>cb(0));};flush();
|
|
22
|
+
expect(document.querySelectorAll('[data-annotation-id]')).toHaveLength(120);
|
|
23
|
+
expect(document.querySelectorAll('[data-papyrus-overlay] button')).toHaveLength(1);
|
|
24
|
+
const group=document.querySelector('[data-annotation-id="n0"]')!;
|
|
25
|
+
expect((group.firstChild as HTMLElement).style.top).toBe('30px');
|
|
26
|
+
expect((group.lastChild as HTMLElement).textContent).toBe('120');
|
|
27
|
+
(group.lastChild as HTMLElement).click();expect(taps).toEqual(['n0']);
|
|
28
|
+
top=80;renderer.schedule();flush();expect(document.querySelector('[data-annotation-id="n0"]')).toBe(group);expect((group.firstChild as HTMLElement).style.top).toBe('90px');
|
|
29
|
+
snapshot=[];renderer.schedule();flush();expect(document.querySelectorAll('[data-annotation-id]')).toHaveLength(0);
|
|
30
|
+
renderer.destroy();expect(document.querySelector('[data-papyrus-overlay]')).toBeNull();window.requestAnimationFrame=originalRAF;
|
|
31
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import {JSDOM} from 'jsdom';
|
|
3
|
+
import {describe,it,expect} from 'vitest';
|
|
4
|
+
const source=readFileSync('packages/ui-react-native/runtime/runtime.js','utf8');
|
|
5
|
+
function fixture() {
|
|
6
|
+
const messages:any[]=[];
|
|
7
|
+
const dom=new JSDOM('<div id="viewer"></div>',{runScripts:'outside-only'});
|
|
8
|
+
dom.window.ReactNativeWebView={postMessage:(raw:string)=>messages.push(JSON.parse(raw))} as never;
|
|
9
|
+
// Expose only the setup boundary in the test copy; execute the production handler.
|
|
10
|
+
dom.window.eval(source.replace("sendMessage({ type: 'ready' });",`
|
|
11
|
+
window.seedMenu=(session,payload,contents)=>{documentSessionId=session;nativeMenuSelection={payload,contents};};`));
|
|
12
|
+
const chapter=new JSDOM('<p>one word and a phrase</p>');
|
|
13
|
+
const text=chapter.window.document.querySelector('p')!.firstChild!;
|
|
14
|
+
const range=chapter.window.document.createRange();range.setStart(text,4);range.setEnd(text,8);
|
|
15
|
+
chapter.window.getSelection()!.addRange(range);
|
|
16
|
+
const payload={documentSessionId:'current',text:'word',pageIndex:1,
|
|
17
|
+
anchor:{kind:'epub-cfi',version:1,cfiRange:'epubcfi(/6/2!/4/2:4)',href:'one.xhtml',quote:'word'}};
|
|
18
|
+
(dom.window as any).seedMenu('current',payload,{document:chapter.window.document,window:chapter.window});
|
|
19
|
+
const send=(key:string,session='current')=>dom.window.dispatchEvent(new dom.window.MessageEvent('message',
|
|
20
|
+
{data:JSON.stringify({type:'epub-selection-action',id:'selection-menu',key,documentSessionId:session})}));
|
|
21
|
+
return {messages,chapter,send,dom};
|
|
22
|
+
}
|
|
23
|
+
describe('native EPUB menu dispatch',()=>{
|
|
24
|
+
it('uses the chapter CFI snapshot even when root selection is empty',()=>{
|
|
25
|
+
const f=fixture();expect(f.dom.window.getSelection()!.toString()).toBe('');f.send('highlight');
|
|
26
|
+
expect(f.messages).toContainEqual(expect.objectContaining({name:'EPUB_SELECTION_ACTION',payload:expect.objectContaining({action:'highlight',text:'word',anchor:expect.objectContaining({href:'one.xhtml'})})}));
|
|
27
|
+
expect(f.chapter.window.getSelection()!.toString()).toBe('');
|
|
28
|
+
});
|
|
29
|
+
it('rejects stale document sessions and unknown actions',()=>{
|
|
30
|
+
const f=fixture();f.send('copy','old');f.send('deleteDocument');expect(f.messages).toHaveLength(0);
|
|
31
|
+
expect(f.chapter.window.getSelection()!.toString()).toBe('word');
|
|
32
|
+
});
|
|
33
|
+
it('selects all inside the chapter, without annotating the previous word',()=>{
|
|
34
|
+
const f=fixture();f.send('selectAll');expect(f.chapter.window.getSelection()!.toString()).toBe('one word and a phrase');expect(f.messages).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
it('consumes each action once',()=>{
|
|
37
|
+
const f=fixture();f.send('comment');f.send('comment');expect(f.messages).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
});
|
package/runtime/index.html
CHANGED
|
@@ -20483,6 +20483,127 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
20483
20483
|
<!-- @papyrus-comic-runtime:end -->
|
|
20484
20484
|
<script>
|
|
20485
20485
|
(function () {
|
|
20486
|
+
/* @papyrus-epub-annotations:start */
|
|
20487
|
+
(function (root, factory) {
|
|
20488
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
20489
|
+
else root.PapyrusEpubAnnotations = factory();
|
|
20490
|
+
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
|
20491
|
+
function validAnchor(anchor) {
|
|
20492
|
+
return anchor && anchor.version === 1 && anchor.kind === 'epub-cfi' &&
|
|
20493
|
+
typeof anchor.cfiRange === 'string' && anchor.cfiRange.length <= 4096 && /^epubcfi\([^\n<>]+\)$/.test(anchor.cfiRange) &&
|
|
20494
|
+
typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
|
|
20495
|
+
typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
|
|
20496
|
+
}
|
|
20497
|
+
function textNodes(document) {
|
|
20498
|
+
var walker = document.createTreeWalker(document.body, 4);
|
|
20499
|
+
var nodes = [], text = '', node;
|
|
20500
|
+
while ((node = walker.nextNode())) {
|
|
20501
|
+
if (node.parentElement.closest('script,style,[data-papyrus-overlay]')) continue;
|
|
20502
|
+
nodes.push({node:node, start:text.length}); text += node.textContent;
|
|
20503
|
+
}
|
|
20504
|
+
return {nodes:nodes,text:text};
|
|
20505
|
+
}
|
|
20506
|
+
function recoverRange(document, anchor) {
|
|
20507
|
+
var source = textNodes(document), match = null, start = source.text.indexOf(anchor.quote);
|
|
20508
|
+
while (start >= 0) {
|
|
20509
|
+
var end = start + anchor.quote.length;
|
|
20510
|
+
if ((!anchor.prefix || source.text.slice(Math.max(0,start-anchor.prefix.length),start) === anchor.prefix) &&
|
|
20511
|
+
(!anchor.suffix || source.text.slice(end,end+anchor.suffix.length) === anchor.suffix)) {
|
|
20512
|
+
if (match) return null; match = {start:start,end:end};
|
|
20513
|
+
}
|
|
20514
|
+
start = source.text.indexOf(anchor.quote,start+1);
|
|
20515
|
+
}
|
|
20516
|
+
if (!match) return null;
|
|
20517
|
+
var range = document.createRange();
|
|
20518
|
+
var first = source.nodes.find(function(n){return n.start+n.node.length > match.start;});
|
|
20519
|
+
var last = source.nodes.find(function(n){return n.start+n.node.length >= match.end;});
|
|
20520
|
+
if (!first || !last) return null;
|
|
20521
|
+
range.setStart(first.node,match.start-first.start); range.setEnd(last.node,match.end-last.start);
|
|
20522
|
+
return range;
|
|
20523
|
+
}
|
|
20524
|
+
function resolveRange(contents, anchor) {
|
|
20525
|
+
if (!validAnchor(anchor)) return null;
|
|
20526
|
+
try { var range = contents.range(anchor.cfiRange); if (range && range.toString() === anchor.quote) return range; } catch (_) {}
|
|
20527
|
+
return recoverRange(contents.document,anchor);
|
|
20528
|
+
}
|
|
20529
|
+
function selection(contents, cfiRange, href, documentId) {
|
|
20530
|
+
var range;
|
|
20531
|
+
try { range = contents.range(cfiRange); } catch (_) { return null; }
|
|
20532
|
+
if (!range || !range.toString()) return null;
|
|
20533
|
+
var before = contents.document.createRange(); before.selectNodeContents(contents.document.body); before.setEnd(range.startContainer,range.startOffset);
|
|
20534
|
+
var after = contents.document.createRange(); after.selectNodeContents(contents.document.body); after.setStart(range.endContainer,range.endOffset);
|
|
20535
|
+
var anchor = {version:1,kind:'epub-cfi',cfiRange:cfiRange,href:href,spineIndex:contents.sectionIndex,documentId:documentId,quote:range.toString(),prefix:before.toString().slice(-48),suffix:after.toString().slice(0,48)};
|
|
20536
|
+
return validAnchor(anchor) ? anchor : null;
|
|
20537
|
+
}
|
|
20538
|
+
function install(contents, options) {
|
|
20539
|
+
var doc = contents.document, win = contents.window, alive = true, frame = 0;
|
|
20540
|
+
var overlay = doc.createElement('div'); overlay.dataset.papyrusOverlay = 'annotations';
|
|
20541
|
+
overlay.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;z-index:3';
|
|
20542
|
+
var originalPosition=doc.body.style.position;
|
|
20543
|
+
if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
|
|
20544
|
+
doc.body.appendChild(overlay);
|
|
20545
|
+
var registry = new Map(), hitRegions = [];
|
|
20546
|
+
function render() {
|
|
20547
|
+
frame = 0; if (!alive || !options.isCurrent()) return;
|
|
20548
|
+
var keep = new Set(), noteGroups = new Map();
|
|
20549
|
+
hitRegions = [];
|
|
20550
|
+
options.annotations().forEach(function(annotation) {
|
|
20551
|
+
var anchor = annotation.anchor;
|
|
20552
|
+
if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
|
|
20553
|
+
var range = resolveRange(contents,anchor); if (!range) return;
|
|
20554
|
+
var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
|
|
20555
|
+
if (!rects.length) return;
|
|
20556
|
+
keep.add(annotation.id);
|
|
20557
|
+
var group = registry.get(annotation.id);
|
|
20558
|
+
if (!group) { group = doc.createElement('div'); group.dataset.annotationId = annotation.id; overlay.appendChild(group); registry.set(annotation.id,group); if (!doc.defaultView.matchMedia || !doc.defaultView.matchMedia('(prefers-reduced-motion: reduce)').matches) { if (typeof group.animate === 'function') group.animate([{opacity:0},{opacity:1}],{duration:180,easing:'ease-out'}); } }
|
|
20559
|
+
group.replaceChildren();
|
|
20560
|
+
hitRegions.push({id:annotation.id,rects:rects});
|
|
20561
|
+
var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
|
|
20562
|
+
var style = annotation.markupStyle || annotation.type;
|
|
20563
|
+
var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
|
|
20564
|
+
rects.forEach(function(rect) {
|
|
20565
|
+
var mark = doc.createElement('div');
|
|
20566
|
+
var bodyRect=doc.body.getBoundingClientRect();
|
|
20567
|
+
var height = style === 'highlight' ? rect.height : 2;
|
|
20568
|
+
var y = style === 'strikeout' ? rect.top+rect.height/2 : style === 'underline' ? rect.bottom-2 : rect.top;
|
|
20569
|
+
mark.style.cssText = 'position:absolute;pointer-events:none;left:'+(rect.left-bodyRect.left)+'px;top:'+(y-bodyRect.top)+'px;width:'+rect.width+'px;height:'+height+'px;background:'+color+';opacity:'+opacity;
|
|
20570
|
+
if (['highlight','underline','strikeout'].includes(style)) group.appendChild(mark);
|
|
20571
|
+
});
|
|
20572
|
+
var hasNote = annotation.noteContent !== undefined || annotation.type === 'comment' || annotation.type === 'text';
|
|
20573
|
+
if (hasNote) {
|
|
20574
|
+
var last = rects[rects.length-1], button = doc.createElement('button');
|
|
20575
|
+
button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
|
|
20576
|
+
button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+Math.max(0,Math.min(doc.documentElement.clientWidth-44,last.right-doc.body.getBoundingClientRect().left))+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
|
|
20577
|
+
var key = Math.floor(last.top/44);
|
|
20578
|
+
var neighbors=noteGroups.get(key);
|
|
20579
|
+
if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
|
|
20580
|
+
neighbors.ids.push(annotation.id);
|
|
20581
|
+
}
|
|
20582
|
+
});
|
|
20583
|
+
noteGroups.forEach(function(group){
|
|
20584
|
+
group.button.textContent=group.ids.length>1?String(group.ids.length):'●';
|
|
20585
|
+
group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
|
|
20586
|
+
});
|
|
20587
|
+
registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
|
|
20588
|
+
}
|
|
20589
|
+
function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
|
|
20590
|
+
function tapMarkup(event) {
|
|
20591
|
+
if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
|
|
20592
|
+
var hit=hitRegions.find(function(item){return item.rects.some(function(r){return event.clientX>=r.left && event.clientX<=r.right && event.clientY>=r.top && event.clientY<=r.bottom;});});
|
|
20593
|
+
if(hit) {event.stopPropagation();options.onTap(hit.id);}
|
|
20594
|
+
}
|
|
20595
|
+
doc.addEventListener('click',tapMarkup);
|
|
20596
|
+
var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
|
|
20597
|
+
if (observer) observer.observe(doc.body);
|
|
20598
|
+
win.addEventListener('resize',schedule); doc.addEventListener('load',schedule,true);
|
|
20599
|
+
if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
|
|
20600
|
+
schedule();
|
|
20601
|
+
return {schedule:schedule, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
|
|
20602
|
+
}
|
|
20603
|
+
return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
|
|
20604
|
+
});
|
|
20605
|
+
|
|
20606
|
+
/* @papyrus-epub-annotations:end */
|
|
20486
20607
|
const viewer = document.getElementById('viewer');
|
|
20487
20608
|
const DEFAULT_FONT_SIZE = 16;
|
|
20488
20609
|
const TEXT_PAGE_CHUNK = 1600;
|
|
@@ -20490,6 +20611,11 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
20490
20611
|
let currentType = null;
|
|
20491
20612
|
let book = null;
|
|
20492
20613
|
let rendition = null;
|
|
20614
|
+
let documentSessionId = null;
|
|
20615
|
+
let annotationDocumentId;
|
|
20616
|
+
let annotationLabels = {};
|
|
20617
|
+
let epubAnnotations = [];
|
|
20618
|
+
const epubAnnotationRenderers = new Map();
|
|
20493
20619
|
let spineItems = [];
|
|
20494
20620
|
let textPages = [];
|
|
20495
20621
|
let textContainer = null;
|
|
@@ -20897,6 +21023,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
20897
21023
|
};
|
|
20898
21024
|
|
|
20899
21025
|
const clearViewer = () => {
|
|
21026
|
+
epubAnnotationRenderers.forEach(renderer => renderer.destroy());
|
|
21027
|
+
epubAnnotationRenderers.clear();
|
|
20900
21028
|
if (epubInteractionCleanup) {
|
|
20901
21029
|
epubInteractionCleanup();
|
|
20902
21030
|
epubInteractionCleanup = null;
|
|
@@ -21554,6 +21682,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21554
21682
|
clearViewer();
|
|
21555
21683
|
rendition = book.renderTo(viewer, {
|
|
21556
21684
|
manager: 'continuous',
|
|
21685
|
+
allowScriptedContent: false,
|
|
21557
21686
|
width: '100%',
|
|
21558
21687
|
height: '100%',
|
|
21559
21688
|
flow: 'scrolled-continuous',
|
|
@@ -21562,10 +21691,22 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21562
21691
|
sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
|
|
21563
21692
|
|
|
21564
21693
|
if (rendition && rendition.hooks && rendition.hooks.content) {
|
|
21694
|
+
const session = documentSessionId;
|
|
21695
|
+
const activeRendition = rendition;
|
|
21565
21696
|
rendition.hooks.content.register((contents) => {
|
|
21697
|
+
const href = spineItems[contents.sectionIndex]?.href;
|
|
21698
|
+
if (href && window.PapyrusEpubAnnotations) {
|
|
21699
|
+
const renderer = window.PapyrusEpubAnnotations.install(contents, {
|
|
21700
|
+
href, get documentId() { return annotationDocumentId; },
|
|
21701
|
+
isCurrent: () => session === documentSessionId && activeRendition === rendition,
|
|
21702
|
+
annotations: () => epubAnnotations, labels: () => annotationLabels,
|
|
21703
|
+
onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
|
|
21704
|
+
});
|
|
21705
|
+
epubAnnotationRenderers.set(contents,renderer);
|
|
21706
|
+
}
|
|
21566
21707
|
const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
|
|
21567
21708
|
if (frame) {
|
|
21568
|
-
frame.setAttribute('sandbox', 'allow-
|
|
21709
|
+
frame.setAttribute('sandbox', 'allow-same-origin');
|
|
21569
21710
|
}
|
|
21570
21711
|
const selectionDocument = contents && contents.document;
|
|
21571
21712
|
const selectionWindow = contents && contents.window;
|
|
@@ -21575,13 +21716,46 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21575
21716
|
selectionWindow &&
|
|
21576
21717
|
typeof selectionWindow.getSelection === 'function'
|
|
21577
21718
|
) {
|
|
21719
|
+
let selectionTimer = null;
|
|
21720
|
+
let lastSelectionKey = '';
|
|
21721
|
+
let lastSelectionAt = 0;
|
|
21722
|
+
const reportSelection = () => {
|
|
21723
|
+
selectionTimer = null;
|
|
21724
|
+
if (session !== documentSessionId || activeRendition !== rendition) return;
|
|
21725
|
+
const selected = selectionWindow.getSelection();
|
|
21726
|
+
if (!selected || selected.isCollapsed || selected.rangeCount === 0 ||
|
|
21727
|
+
typeof contents.cfiFromRange !== 'function') return;
|
|
21728
|
+
const range = selected.getRangeAt(0).cloneRange();
|
|
21729
|
+
if (!range.toString().trim()) return;
|
|
21730
|
+
const href = spineItems[contents.sectionIndex]?.href;
|
|
21731
|
+
if (!href) return;
|
|
21732
|
+
let cfi;
|
|
21733
|
+
try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
|
|
21734
|
+
const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
|
|
21735
|
+
if (!anchor) return;
|
|
21736
|
+
const key = anchor.cfiRange + ':' + anchor.quote;
|
|
21737
|
+
const now = Date.now();
|
|
21738
|
+
if (key === lastSelectionKey && now - lastSelectionAt < 750) return;
|
|
21739
|
+
lastSelectionKey = key;
|
|
21740
|
+
lastSelectionAt = now;
|
|
21741
|
+
const payload = {documentSessionId: session, text: anchor.quote,
|
|
21742
|
+
pageIndex: contents.sectionIndex + 1, anchor};
|
|
21743
|
+
nativeMenuSelection = {contents, payload};
|
|
21744
|
+
sendEvent('EPUB_TEXT_SELECTED', payload);
|
|
21745
|
+
};
|
|
21578
21746
|
const handleSelectionChange = () => {
|
|
21579
21747
|
const selection = selectionWindow.getSelection();
|
|
21580
21748
|
const text = selection ? selection.toString().trim() : '';
|
|
21581
21749
|
epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
|
|
21750
|
+
if (selectionTimer !== null) clearTimeout(selectionTimer);
|
|
21751
|
+
// epub.js "selected" is not reliable on every iOS WKWebView path.
|
|
21752
|
+
// The content document still emits selectionchange, so resolve its
|
|
21753
|
+
// live Range to a CFI before the native menu action can fire.
|
|
21754
|
+
if (text) reportSelection();
|
|
21582
21755
|
};
|
|
21583
21756
|
selectionDocument.addEventListener('selectionchange', handleSelectionChange);
|
|
21584
21757
|
const selectionCleanup = () => {
|
|
21758
|
+
if (selectionTimer !== null) clearTimeout(selectionTimer);
|
|
21585
21759
|
selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
|
|
21586
21760
|
epubSelectionCleanups.delete(selectionCleanup);
|
|
21587
21761
|
};
|
|
@@ -21602,21 +21776,28 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21602
21776
|
rendition.hooks.unloaded.register((view) => {
|
|
21603
21777
|
const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
|
|
21604
21778
|
if (typeof cleanup === 'function') cleanup();
|
|
21779
|
+
const renderer = epubAnnotationRenderers.get(view?.contents);
|
|
21780
|
+
if (renderer) renderer.destroy();
|
|
21781
|
+
epubAnnotationRenderers.delete(view?.contents);
|
|
21605
21782
|
});
|
|
21606
21783
|
}
|
|
21607
21784
|
|
|
21608
21785
|
if (rendition && typeof rendition.on === 'function') {
|
|
21786
|
+
const selectionSession = documentSessionId;
|
|
21787
|
+
const selectionRendition = rendition;
|
|
21609
21788
|
rendition.on('selected', (cfiRange, contents) => {
|
|
21610
|
-
|
|
21611
|
-
const
|
|
21612
|
-
|
|
21613
|
-
if (
|
|
21614
|
-
|
|
21615
|
-
}
|
|
21616
|
-
|
|
21617
|
-
|
|
21618
|
-
}
|
|
21789
|
+
if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
|
|
21790
|
+
const href = spineItems[contents.sectionIndex]?.href;
|
|
21791
|
+
const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
|
|
21792
|
+
if (!anchor) return;
|
|
21793
|
+
epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
|
|
21794
|
+
const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
|
|
21795
|
+
nativeMenuSelection = {contents, payload};
|
|
21796
|
+
sendEvent('EPUB_TEXT_SELECTED', payload);
|
|
21619
21797
|
});
|
|
21798
|
+
['rendered','resized','layout'].forEach(name => rendition.on(name, () => {
|
|
21799
|
+
if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
|
|
21800
|
+
}));
|
|
21620
21801
|
rendition.on('relocated', (location) => {
|
|
21621
21802
|
const start = location && location.start;
|
|
21622
21803
|
let index = start && Number.isInteger(start.index) ? start.index : -1;
|
|
@@ -21746,7 +21927,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21746
21927
|
const end = getTouchPoint(event);
|
|
21747
21928
|
const start = touchStart;
|
|
21748
21929
|
touchStart = null;
|
|
21749
|
-
|
|
21930
|
+
const selectedText = event?.target?.ownerDocument?.defaultView?.getSelection?.()?.toString().trim() || '';
|
|
21931
|
+
if (!selectedText && start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
|
|
21750
21932
|
sendEvent('VIEWER_TAP', {});
|
|
21751
21933
|
}
|
|
21752
21934
|
epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
|
|
@@ -21903,6 +22085,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21903
22085
|
}
|
|
21904
22086
|
};
|
|
21905
22087
|
|
|
22088
|
+
let nativeMenuSelection = null;
|
|
22089
|
+
|
|
21906
22090
|
const handleCommand = async (message) => {
|
|
21907
22091
|
const { id, kind, payload } = message;
|
|
21908
22092
|
|
|
@@ -21910,6 +22094,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21910
22094
|
if (kind === 'load') {
|
|
21911
22095
|
const generation = (epubLoadGeneration += 1);
|
|
21912
22096
|
currentType = payload.type;
|
|
22097
|
+
documentSessionId = payload.documentSessionId || null;
|
|
22098
|
+
epubAnnotations = [];
|
|
22099
|
+
annotationDocumentId = undefined;
|
|
21913
22100
|
zoom = 1.0;
|
|
21914
22101
|
|
|
21915
22102
|
if (currentType === 'text') {
|
|
@@ -21950,6 +22137,36 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
21950
22137
|
throw new Error('Unsupported document type');
|
|
21951
22138
|
}
|
|
21952
22139
|
|
|
22140
|
+
if (kind === 'epub-annotations' || kind === 'epub-annotation-navigate') {
|
|
22141
|
+
if (currentType !== 'epub' || !documentSessionId || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB annotation session');
|
|
22142
|
+
if (kind === 'epub-annotations') {
|
|
22143
|
+
const incoming = payload.annotations;
|
|
22144
|
+
if (!Array.isArray(incoming) || incoming.length > 10000 || incoming.some(a => !a || typeof a.id !== 'string' || a.id.length > 256)) throw new Error('Invalid EPUB annotations');
|
|
22145
|
+
const ids = new Set();
|
|
22146
|
+
epubAnnotations = incoming.filter(a => {
|
|
22147
|
+
if (ids.has(a.id) || !window.PapyrusEpubAnnotations.validAnchor(a.anchor)) return false;
|
|
22148
|
+
ids.add(a.id); return true;
|
|
22149
|
+
});
|
|
22150
|
+
annotationDocumentId = typeof payload.documentId === 'string' ? payload.documentId : undefined;
|
|
22151
|
+
annotationLabels = {comment: typeof payload.labels?.comment === 'string' ? payload.labels.comment.slice(0,128) : ''};
|
|
22152
|
+
epubAnnotationRenderers.forEach(renderer => renderer.schedule());
|
|
22153
|
+
} else {
|
|
22154
|
+
const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
|
|
22155
|
+
if (!annotation) throw new Error('Annotation not available');
|
|
22156
|
+
// Navigation uses the CFI, never a synthetic percentage/page rectangle.
|
|
22157
|
+
const navigationSession = documentSessionId;
|
|
22158
|
+
const activeRendition = rendition;
|
|
22159
|
+
// Open the confirmed chapter first, resolve its quote, then navigate its actual range.
|
|
22160
|
+
await activeRendition.display(annotation.anchor.href);
|
|
22161
|
+
if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
|
|
22162
|
+
const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
|
|
22163
|
+
const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
|
|
22164
|
+
if(!range)throw new Error('EPUB anchor unresolved');
|
|
22165
|
+
await activeRendition.display(contents.cfiFromRange(range));
|
|
22166
|
+
}
|
|
22167
|
+
sendResponse(id,true,null); return;
|
|
22168
|
+
}
|
|
22169
|
+
|
|
21953
22170
|
if (kind === 'go-to-page') {
|
|
21954
22171
|
const page = Math.max(1, payload.page || 1);
|
|
21955
22172
|
if (currentType === 'text') {
|
|
@@ -22119,6 +22336,26 @@ https://github.com/nodeca/pako/blob/main/LICENSE
|
|
|
22119
22336
|
return;
|
|
22120
22337
|
}
|
|
22121
22338
|
|
|
22339
|
+
if (message.type === 'epub-selection-action') {
|
|
22340
|
+
const selected = nativeMenuSelection;
|
|
22341
|
+
if (!selected || message.documentSessionId !== documentSessionId ||
|
|
22342
|
+
selected.payload.documentSessionId !== documentSessionId) return;
|
|
22343
|
+
if (message.key === 'selectAll') {
|
|
22344
|
+
const doc = selected.contents.document;
|
|
22345
|
+
const range = doc.createRange();
|
|
22346
|
+
range.selectNodeContents(doc.body);
|
|
22347
|
+
const selection = selected.contents.window.getSelection();
|
|
22348
|
+
selection.removeAllRanges();
|
|
22349
|
+
selection.addRange(range);
|
|
22350
|
+
return;
|
|
22351
|
+
}
|
|
22352
|
+
if (!['copy','define','highlight','underline','strikeout','comment'].includes(message.key)) return;
|
|
22353
|
+
sendEvent('EPUB_SELECTION_ACTION', {...selected.payload, action: message.key});
|
|
22354
|
+
selected.contents.window.getSelection()?.removeAllRanges();
|
|
22355
|
+
nativeMenuSelection = null;
|
|
22356
|
+
return;
|
|
22357
|
+
}
|
|
22358
|
+
|
|
22122
22359
|
if (!message.kind) return;
|
|
22123
22360
|
handleCommand(message);
|
|
22124
22361
|
};
|