@papyrus-sdk/ui-react-native 0.2.35-beta.11.epub.1 → 0.2.35-beta.11.epub.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyrus-sdk/ui-react-native",
3
- "version": "0.2.35-beta.11.epub.1",
3
+ "version": "0.2.35-beta.11.epub.2",
4
4
  "license": "MIT",
5
5
  "keywords": [
6
6
  "papyrus",
@@ -188,7 +188,7 @@ describe("mobile comic runtime helpers", () => {
188
188
  expect(outlineGuard).toBeLessThan(readyEvent);
189
189
  });
190
190
 
191
- it("uses continuous scrolling for EPUB rendition", () => {
191
+ it("ships the paginated and chapter-scroll EPUB reading policy", () => {
192
192
  const runtime = readFileSync(
193
193
  resolve(process.cwd(), "packages/ui-react-native/runtime/runtime.js"),
194
194
  "utf8"
@@ -198,18 +198,18 @@ describe("mobile comic runtime helpers", () => {
198
198
  "utf8"
199
199
  );
200
200
 
201
- expect(runtime).toContain("manager: 'continuous'");
202
- expect(html).toContain("manager: 'continuous'");
203
- expect(runtime).toContain("flow: 'scrolled-continuous'");
204
- expect(html).toContain("flow: 'scrolled-continuous'");
201
+ expect(runtime).toContain("manager:'default'");
202
+ expect(html).toContain("manager:'default'");
203
+ expect(runtime).toContain("'scrolled-doc'");
204
+ expect(html).toContain("'scrolled-doc'");
205
205
  expect(runtime).toContain("rendition.on('relocated'");
206
206
  expect(html).toContain("rendition.on('relocated'");
207
207
  expect(runtime).toContain("sendEvent('VIEWER_SCROLL'");
208
208
  expect(html).toContain("sendEvent('VIEWER_SCROLL'");
209
- expect(runtime).toContain("sendEvent('VIEWER_TAP'");
210
- expect(html).toContain("sendEvent('VIEWER_TAP'");
211
- expect(runtime).not.toContain("flow: 'paginated'");
212
- expect(html).not.toContain("flow: 'paginated'");
209
+ expect(runtime).toContain("sendEvent('EPUB_CONTENT_TAP'");
210
+ expect(html).toContain("sendEvent('EPUB_CONTENT_TAP'");
211
+ expect(runtime).toContain("'paginated'");
212
+ expect(html).toContain("'paginated'");
213
213
  });
214
214
 
215
215
  it("observes the real EPUB manager queue without changing queue semantics", () => {
@@ -9,13 +9,36 @@
9
9
  if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
10
10
  return null;
11
11
  }
12
- function location(raw,count,readingMode){
12
+ function location(raw,count,readingMode,geometry){
13
13
  var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
14
14
  var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
15
15
  var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
16
16
  var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
17
+ var atStart=Boolean(raw.atStart),atEnd=Boolean(raw.atEnd);
18
+ // Chromium may round a column offset down by a fraction of a CSS pixel.
19
+ // Keep epub.js authoritative outside a 1px boundary, and for RTL layouts.
20
+ if(visualPage!==null && total>0 && geometry && geometry.direction==='ltr' && geometry.pageWidth>0 && Number.isFinite(geometry.offset)){
21
+ var boundary=Math.round(geometry.offset/geometry.pageWidth);
22
+ if(Math.abs(geometry.offset-boundary*geometry.pageWidth)<=1){
23
+ visualPage=Math.min(total,Math.max(1,boundary+1));
24
+ atStart=chapter===(geometry.firstIndex ?? 0)+1 && visualPage===1;
25
+ atEnd=chapter===(geometry.lastIndex ?? count-1)+1 && visualPage===total;
26
+ }
27
+ }
17
28
  var fraction=total?Math.max(0,(visualPage-1)/total):0;
18
- return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:Boolean(raw.atStart),atEnd:Boolean(raw.atEnd)};
29
+ return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:atStart,atEnd:atEnd};
30
+ }
31
+ function installResizeAnchor(manager,readCfi){
32
+ var original=manager.resize,anchor=null;
33
+ function resize(width,height,target){
34
+ if(validCfi(target))anchor=target;
35
+ if(!anchor){var current=readCfi();if(validCfi(current))anchor=current;}
36
+ // epub.js accepts a logical target as its third argument. Keep it across
37
+ // keyboard/dialog reflows instead of adopting each new viewport start.
38
+ return original.call(this,width,height,anchor || target);
39
+ }
40
+ manager.resize=resize;
41
+ return {cfi:function(){return anchor;},invalidate:function(){anchor=null;},destroy:function(){if(manager.resize===resize)manager.resize=original;anchor=null;}};
19
42
  }
20
43
  function install(contents,config){
21
44
  var doc=contents.document,win=contents.window,start=null,blocked=false;
@@ -23,11 +46,13 @@
23
46
  function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
24
47
  function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
25
48
  function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
26
- function move(e){if(e.touches.length!==1)blocked=true;}
27
- function end(e){var action=gesture(start,point(e),{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
49
+ function invalidateScroll(finish){if(config.current() && start && finish && config.mode()==='chapter-scroll' && !blocked && !selected() && Math.abs(finish.y-start.y)>12 && Math.abs(finish.y-start.y)>Math.abs(finish.x-start.x) && config.interact)config.interact();}
50
+ function move(e){if(e.touches.length!==1)blocked=true;invalidateScroll(point(e));}
51
+ function end(e){var finish=point(e);invalidateScroll(finish);var action=gesture(start,finish,{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
28
52
  function cancel(){start=null;blocked=true;}
29
- doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});
30
- return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);};
53
+ function link(e){if(config.current() && e.target?.closest?.('a') && config.interact)config.interact();}
54
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});doc.addEventListener('click',link);
55
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);doc.removeEventListener('click',link);};
31
56
  }
32
- root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,install:install};
57
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,installResizeAnchor:installResizeAnchor,install:install};
33
58
  })(typeof window==='undefined'?globalThis:window);
@@ -20,3 +20,55 @@ describe('EPUB iframe gestures', () => {
20
20
  touch(d.window.document.querySelector('p')!);expect(taps).toBe(0);hit=false;touch(d.window.document.querySelector('a')!);expect(taps).toBe(0);touch(d.window.document.querySelector('p')!);expect(taps).toBe(1);cleanup();touch(d.window.document.querySelector('p')!);expect(taps).toBe(1);
21
21
  });
22
22
  });
23
+
24
+ describe('EPUB subpixel column boundaries',()=>{
25
+ it('normalizes only a nearby LTR boundary and updates start/end',()=>{
26
+ const p=load(),raw={start:{index:0,cfi:'epubcfi(/6/2!/4/12)',displayed:{page:1,total:17}},atStart:true,atEnd:false};
27
+ expect(p.location(raw,3,'paged',{offset:411.4285888671875,pageWidth:412,direction:'ltr'})).toMatchObject({visualPage:2,atStart:false});
28
+ expect(p.location({...raw,start:{...raw.start,index:1}},4,'paged',{offset:0,pageWidth:412,direction:'ltr',firstIndex:1,lastIndex:2})).toMatchObject({atStart:true});
29
+ expect(p.location(raw,3,'paged',{offset:400,pageWidth:412,direction:'ltr'}).visualPage).toBe(1);
30
+ expect(p.location(raw,3,'paged',{offset:411.5,pageWidth:412,direction:'rtl'}).visualPage).toBe(1);
31
+ expect(p.location({...raw,start:{...raw.start,index:2,displayed:{page:16,total:17}}},3,'paged',{offset:6591.5,pageWidth:412,direction:'ltr'})).toMatchObject({visualPage:17,atEnd:true});
32
+ });
33
+ });
34
+
35
+ describe('EPUB resize anchors',()=>{
36
+ it('preserves a logical CFI through transient resize until real interaction',()=>{
37
+ const p=load(),calls:any[]=[];let current='epubcfi(/6/2!/4/12/1:149)';
38
+ const original=function(this:any,w:any,h:any,target:any){calls.push([this,w,h,target]);return 'resized';};
39
+ const manager={resize:original};const anchor=p.installResizeAnchor(manager,()=>current);
40
+ expect(manager.resize(412,889,undefined)).toBe('resized');
41
+ current='epubcfi(/6/2!/4/12/1:97)';manager.resize(412,863,undefined);
42
+ expect(calls.map(c=>c[3])).toEqual(['epubcfi(/6/2!/4/12/1:149)','epubcfi(/6/2!/4/12/1:149)']);
43
+ expect(anchor.cfi()).toBe('epubcfi(/6/2!/4/12/1:149)');anchor.invalidate();
44
+ current='epubcfi(/6/2!/4/22/1:149)';manager.resize(863,412,undefined);
45
+ expect(calls.at(-1)[3]).toBe(current);expect(calls.every(c=>c[0]===manager)).toBe(true);
46
+ anchor.destroy();expect(manager.resize).toBe(original);
47
+ });
48
+ it('honors explicit valid targets and does not pass invalid CFIs',()=>{
49
+ const p=load(),calls:any[]=[];const manager={resize:(w:any,h:any,target:any)=>calls.push(target)};
50
+ const anchor=p.installResizeAnchor(manager,()=>null);manager.resize(1,2,undefined);expect(calls[0]).toBeUndefined();
51
+ manager.resize(1,2,'epubcfi(/6/4!/4/2)');expect(anchor.cfi()).toBe('epubcfi(/6/4!/4/2)');anchor.destroy();
52
+ });
53
+ });
54
+
55
+ it('does not discard a resize anchor for tap, selection or note marker',()=>{
56
+ const d=new JSDOM('<p>text</p><a href="#">link</a><button data-papyrus-overlay>note</button>',{runScripts:'outside-only'});
57
+ d.window.eval(readFileSync('packages/ui-react-native/runtime/epubReader.js','utf8'));
58
+ let invalidations=0,readingMode='paged';const p=(d.window as any).PapyrusEpubReader;
59
+ const cleanup=p.install({document:d.window.document,window:d.window},{mode:()=>readingMode,rtl:()=>false,blocked:()=>false,current:()=>true,tap:()=>{},turn:()=>invalidations++,interact:()=>invalidations++});
60
+ function touch(target:Element,y=40){for(const type of ['touchstart','touchend']){const point={clientX:30,clientY:type==='touchend'?y:40};const e=new d.window.Event(type,{bubbles:true});Object.defineProperties(e,{touches:{value:type==='touchend'?[]:[point]},changedTouches:{value:[point]}});target.dispatchEvent(e);}}
61
+ touch(d.window.document.querySelector('p')!);touch(d.window.document.querySelector('button')!);expect(invalidations).toBe(0);
62
+ readingMode='chapter-scroll';touch(d.window.document.querySelector('p')!,200);expect(invalidations).toBe(1);
63
+ d.window.document.querySelector('a')!.dispatchEvent(new d.window.Event('click',{bubbles:true}));expect(invalidations).toBe(2);cleanup();
64
+ });
65
+
66
+ it('invalidates chapter-scroll before relocation and ignores stale iframe callbacks',()=>{
67
+ const d=new JSDOM('<p>text</p><a href="#">link</a>',{runScripts:'outside-only'});
68
+ d.window.eval(readFileSync('packages/ui-react-native/runtime/epubReader.js','utf8'));
69
+ let invalidations=0,current=true;
70
+ const cleanup=(d.window as any).PapyrusEpubReader.install({document:d.window.document,window:d.window},{mode:()=> 'chapter-scroll',rtl:()=>false,blocked:()=>false,current:()=>current,tap:()=>{},turn:()=>{},interact:()=>invalidations++});
71
+ function dispatch(type:string,y:number){const point={clientX:30,clientY:y},e=new d.window.Event(type,{bubbles:true});Object.defineProperties(e,{touches:{value:type==='touchend'?[]:[point]},changedTouches:{value:[point]}});d.window.document.querySelector('p')!.dispatchEvent(e);}
72
+ dispatch('touchstart',40);dispatch('touchmove',120);expect(invalidations).toBe(1);
73
+ current=false;dispatch('touchend',180);d.window.document.querySelector('a')!.dispatchEvent(new d.window.Event('click',{bubbles:true}));expect(invalidations).toBe(1);cleanup();
74
+ });
@@ -20495,13 +20495,36 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20495
20495
  if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
20496
20496
  return null;
20497
20497
  }
20498
- function location(raw,count,readingMode){
20498
+ function location(raw,count,readingMode,geometry){
20499
20499
  var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
20500
20500
  var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
20501
20501
  var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
20502
20502
  var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
20503
+ var atStart=Boolean(raw.atStart),atEnd=Boolean(raw.atEnd);
20504
+ // Chromium may round a column offset down by a fraction of a CSS pixel.
20505
+ // Keep epub.js authoritative outside a 1px boundary, and for RTL layouts.
20506
+ if(visualPage!==null && total>0 && geometry && geometry.direction==='ltr' && geometry.pageWidth>0 && Number.isFinite(geometry.offset)){
20507
+ var boundary=Math.round(geometry.offset/geometry.pageWidth);
20508
+ if(Math.abs(geometry.offset-boundary*geometry.pageWidth)<=1){
20509
+ visualPage=Math.min(total,Math.max(1,boundary+1));
20510
+ atStart=chapter===(geometry.firstIndex ?? 0)+1 && visualPage===1;
20511
+ atEnd=chapter===(geometry.lastIndex ?? count-1)+1 && visualPage===total;
20512
+ }
20513
+ }
20503
20514
  var fraction=total?Math.max(0,(visualPage-1)/total):0;
20504
- return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:Boolean(raw.atStart),atEnd:Boolean(raw.atEnd)};
20515
+ return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:atStart,atEnd:atEnd};
20516
+ }
20517
+ function installResizeAnchor(manager,readCfi){
20518
+ var original=manager.resize,anchor=null;
20519
+ function resize(width,height,target){
20520
+ if(validCfi(target))anchor=target;
20521
+ if(!anchor){var current=readCfi();if(validCfi(current))anchor=current;}
20522
+ // epub.js accepts a logical target as its third argument. Keep it across
20523
+ // keyboard/dialog reflows instead of adopting each new viewport start.
20524
+ return original.call(this,width,height,anchor || target);
20525
+ }
20526
+ manager.resize=resize;
20527
+ return {cfi:function(){return anchor;},invalidate:function(){anchor=null;},destroy:function(){if(manager.resize===resize)manager.resize=original;anchor=null;}};
20505
20528
  }
20506
20529
  function install(contents,config){
20507
20530
  var doc=contents.document,win=contents.window,start=null,blocked=false;
@@ -20509,13 +20532,15 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20509
20532
  function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
20510
20533
  function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
20511
20534
  function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
20512
- function move(e){if(e.touches.length!==1)blocked=true;}
20513
- function end(e){var action=gesture(start,point(e),{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
20535
+ function invalidateScroll(finish){if(config.current() && start && finish && config.mode()==='chapter-scroll' && !blocked && !selected() && Math.abs(finish.y-start.y)>12 && Math.abs(finish.y-start.y)>Math.abs(finish.x-start.x) && config.interact)config.interact();}
20536
+ function move(e){if(e.touches.length!==1)blocked=true;invalidateScroll(point(e));}
20537
+ function end(e){var finish=point(e);invalidateScroll(finish);var action=gesture(start,finish,{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
20514
20538
  function cancel(){start=null;blocked=true;}
20515
- doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});
20516
- return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);};
20539
+ function link(e){if(config.current() && e.target?.closest?.('a') && config.interact)config.interact();}
20540
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});doc.addEventListener('click',link);
20541
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);doc.removeEventListener('click',link);};
20517
20542
  }
20518
- root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,install:install};
20543
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,installResizeAnchor:installResizeAnchor,install:install};
20519
20544
  })(typeof window==='undefined'?globalThis:window);
20520
20545
 
20521
20546
  /* @papyrus-epub-reader:end */
@@ -20650,6 +20675,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20650
20675
  let currentType = null;
20651
20676
  let book = null;
20652
20677
  let rendition = null;
20678
+ let epubResizeAnchor = null;
20653
20679
  let documentSessionId = null;
20654
20680
  let annotationDocumentId;
20655
20681
  let annotationLabels = {};
@@ -21062,6 +21088,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21062
21088
  };
21063
21089
 
21064
21090
  const clearViewer = () => {
21091
+ if(epubResizeAnchor){epubResizeAnchor.destroy();epubResizeAnchor=null;}
21065
21092
  epubAnnotationRenderers.forEach(renderer => renderer.destroy());
21066
21093
  epubAnnotationRenderers.clear();
21067
21094
  if (epubInteractionCleanup) {
@@ -21749,10 +21776,12 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21749
21776
  blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
21750
21777
  annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
21751
21778
  current: () => session === documentSessionId && activeRendition === rendition,
21779
+ interact: () => epubResizeAnchor?.invalidate(),
21752
21780
  tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
21753
21781
  turn: direction => {
21754
21782
  epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
21755
21783
  if(session !== documentSessionId || activeRendition !== rendition)return;
21784
+ epubResizeAnchor?.invalidate();
21756
21785
  return direction<0?activeRendition.prev():activeRendition.next();
21757
21786
  });
21758
21787
  epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
@@ -21870,8 +21899,18 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21870
21899
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
21871
21900
  if (index < 0 || index >= spineItems.length) return;
21872
21901
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
21873
- epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll');
21902
+ let pageGeometry;
21903
+ const manager=rendition.manager,layout=manager?.layout;
21904
+ if(epubViewMode==='single' && layout?.divisor===1 && !manager?.settings?.fullsize && manager?.settings?.direction==='ltr'){
21905
+ const visible=manager.visible?.();
21906
+ const view=visible?.find(view=>view.index===index);
21907
+ if(view && manager.container){
21908
+ pageGeometry={direction:'ltr',firstIndex:book.spine.first()?.index,lastIndex:book.spine.last()?.index,pageWidth:layout.pageWidth,offset:manager.container.getBoundingClientRect().left-view.position().left};
21909
+ }
21910
+ }
21911
+ epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll',pageGeometry);
21874
21912
  if(epubLocation){
21913
+ epubLocation.cfi=epubResizeAnchor?.cfi() || epubLocation.cfi;
21875
21914
  if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
21876
21915
  sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
21877
21916
  }
@@ -21891,6 +21930,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21891
21930
  );
21892
21931
  if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21893
21932
  sendEpubDiagnostic('epub.display.end', diagnosticPayload);
21933
+ const resizeRendition=rendition;
21934
+ epubResizeAnchor=window.PapyrusEpubReader.installResizeAnchor(resizeRendition.manager,()=>resizeRendition.location?.start?.cfi);
21894
21935
  if (
21895
21936
  rendition &&
21896
21937
  rendition.manager &&
@@ -22024,6 +22065,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22024
22065
  const request = ++epubNavigationRequest;
22025
22066
  const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22026
22067
  if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22068
+ epubResizeAnchor?.invalidate();
22027
22069
  await activeRendition.display(target);
22028
22070
  if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22029
22071
  currentPage = pageIndex + 1;
@@ -22213,6 +22255,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22213
22255
  epubNavigationRequest += 1;
22214
22256
  const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22215
22257
  if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22258
+ epubResizeAnchor?.invalidate();
22216
22259
  await activeRendition.display(annotation.anchor.href);
22217
22260
  if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22218
22261
  const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
@@ -22254,6 +22297,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22254
22297
  const active = rendition;
22255
22298
  epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
22256
22299
  if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
22300
+ epubResizeAnchor?.invalidate();
22257
22301
  return payload.direction < 0 ? active.prev() : active.next();
22258
22302
  });
22259
22303
  await epubNavigationQueue;
@@ -22266,7 +22310,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22266
22310
  {
22267
22311
  epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22268
22312
  if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
22269
- const target=epubLocation?.cfi;
22313
+ const target=epubResizeAnchor?.cfi() || epubLocation?.cfi;
22270
22314
  epubViewMode=next;
22271
22315
  await renderEpubRendition('layout',epubLoadGeneration,target);
22272
22316
  });
@@ -22276,7 +22320,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22276
22320
  }
22277
22321
  if(kind === 'epub-go-to-location'){
22278
22322
  if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
22279
- epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>payload.documentSessionId===documentSessionId?rendition.display(payload.cfi):undefined);
22323
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>{if(payload.documentSessionId!==documentSessionId)return;epubResizeAnchor?.invalidate();return rendition.display(payload.cfi);});
22280
22324
  await epubNavigationQueue;sendResponse(id,true,null);return;
22281
22325
  }
22282
22326
 
@@ -22287,7 +22331,7 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22287
22331
  epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22288
22332
  if(session!==documentSessionId)return;
22289
22333
  const active=rendition;
22290
- const cfi=epubLocation?.cfi;
22334
+ const cfi=epubResizeAnchor?.cfi() || epubLocation?.cfi;
22291
22335
  zoom=requestedZoom;applyEpubZoom();
22292
22336
  // Let the content CSS update before redisplaying the logical anchor.
22293
22337
  await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));
@@ -121,13 +121,36 @@
121
121
  if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
122
122
  return null;
123
123
  }
124
- function location(raw,count,readingMode){
124
+ function location(raw,count,readingMode,geometry){
125
125
  var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
126
126
  var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
127
127
  var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
128
128
  var total=mode(readingMode)==='paged' && Number.isFinite(displayed.total)?displayed.total:null;
129
+ var atStart=Boolean(raw.atStart),atEnd=Boolean(raw.atEnd);
130
+ // Chromium may round a column offset down by a fraction of a CSS pixel.
131
+ // Keep epub.js authoritative outside a 1px boundary, and for RTL layouts.
132
+ if(visualPage!==null && total>0 && geometry && geometry.direction==='ltr' && geometry.pageWidth>0 && Number.isFinite(geometry.offset)){
133
+ var boundary=Math.round(geometry.offset/geometry.pageWidth);
134
+ if(Math.abs(geometry.offset-boundary*geometry.pageWidth)<=1){
135
+ visualPage=Math.min(total,Math.max(1,boundary+1));
136
+ atStart=chapter===(geometry.firstIndex ?? 0)+1 && visualPage===1;
137
+ atEnd=chapter===(geometry.lastIndex ?? count-1)+1 && visualPage===total;
138
+ }
139
+ }
129
140
  var fraction=total?Math.max(0,(visualPage-1)/total):0;
130
- return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:Boolean(raw.atStart),atEnd:Boolean(raw.atEnd)};
141
+ return {version:1,cfi:start.cfi,href:start.href||'',chapter:chapter,chapterCount:count,visualPage:visualPage,visualPageCount:total,progress:Math.min(1,Math.max(0,(chapter-1+fraction)/Math.max(1,count))),mode:mode(readingMode),atStart:atStart,atEnd:atEnd};
142
+ }
143
+ function installResizeAnchor(manager,readCfi){
144
+ var original=manager.resize,anchor=null;
145
+ function resize(width,height,target){
146
+ if(validCfi(target))anchor=target;
147
+ if(!anchor){var current=readCfi();if(validCfi(current))anchor=current;}
148
+ // epub.js accepts a logical target as its third argument. Keep it across
149
+ // keyboard/dialog reflows instead of adopting each new viewport start.
150
+ return original.call(this,width,height,anchor || target);
151
+ }
152
+ manager.resize=resize;
153
+ return {cfi:function(){return anchor;},invalidate:function(){anchor=null;},destroy:function(){if(manager.resize===resize)manager.resize=original;anchor=null;}};
131
154
  }
132
155
  function install(contents,config){
133
156
  var doc=contents.document,win=contents.window,start=null,blocked=false;
@@ -135,13 +158,15 @@
135
158
  function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
136
159
  function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
137
160
  function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
138
- function move(e){if(e.touches.length!==1)blocked=true;}
139
- function end(e){var action=gesture(start,point(e),{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
161
+ function invalidateScroll(finish){if(config.current() && start && finish && config.mode()==='chapter-scroll' && !blocked && !selected() && Math.abs(finish.y-start.y)>12 && Math.abs(finish.y-start.y)>Math.abs(finish.x-start.x) && config.interact)config.interact();}
162
+ function move(e){if(e.touches.length!==1)blocked=true;invalidateScroll(point(e));}
163
+ function end(e){var finish=point(e);invalidateScroll(finish);var action=gesture(start,finish,{mode:config.mode(),rtl:config.rtl(),selected:selected(),interactive:blocked||interactive(e.target)||Boolean(config.annotationHit && config.annotationHit(point(e))),multitouch:blocked});start=null;if(!config.current())return;if(action==='tap')config.tap();else if(action)config.turn(action==='next'?1:-1);}
140
164
  function cancel(){start=null;blocked=true;}
141
- doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});
142
- return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);};
165
+ function link(e){if(config.current() && e.target?.closest?.('a') && config.interact)config.interact();}
166
+ doc.addEventListener('touchstart',begin,{passive:true});doc.addEventListener('touchmove',move,{passive:true});doc.addEventListener('touchend',end,{passive:true});doc.addEventListener('touchcancel',cancel,{passive:true});doc.addEventListener('click',link);
167
+ return function(){doc.removeEventListener('touchstart',begin);doc.removeEventListener('touchmove',move);doc.removeEventListener('touchend',end);doc.removeEventListener('touchcancel',cancel);doc.removeEventListener('click',link);};
143
168
  }
144
- root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,install:install};
169
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,installResizeAnchor:installResizeAnchor,install:install};
145
170
  })(typeof window==='undefined'?globalThis:window);
146
171
 
147
172
  /* @papyrus-epub-reader:end */
@@ -276,6 +301,7 @@
276
301
  let currentType = null;
277
302
  let book = null;
278
303
  let rendition = null;
304
+ let epubResizeAnchor = null;
279
305
  let documentSessionId = null;
280
306
  let annotationDocumentId;
281
307
  let annotationLabels = {};
@@ -688,6 +714,7 @@
688
714
  };
689
715
 
690
716
  const clearViewer = () => {
717
+ if(epubResizeAnchor){epubResizeAnchor.destroy();epubResizeAnchor=null;}
691
718
  epubAnnotationRenderers.forEach(renderer => renderer.destroy());
692
719
  epubAnnotationRenderers.clear();
693
720
  if (epubInteractionCleanup) {
@@ -1375,10 +1402,12 @@
1375
1402
  blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
1376
1403
  annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
1377
1404
  current: () => session === documentSessionId && activeRendition === rendition,
1405
+ interact: () => epubResizeAnchor?.invalidate(),
1378
1406
  tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
1379
1407
  turn: direction => {
1380
1408
  epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
1381
1409
  if(session !== documentSessionId || activeRendition !== rendition)return;
1410
+ epubResizeAnchor?.invalidate();
1382
1411
  return direction<0?activeRendition.prev():activeRendition.next();
1383
1412
  });
1384
1413
  epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
@@ -1496,8 +1525,18 @@
1496
1525
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
1497
1526
  if (index < 0 || index >= spineItems.length) return;
1498
1527
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
1499
- epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll');
1528
+ let pageGeometry;
1529
+ const manager=rendition.manager,layout=manager?.layout;
1530
+ if(epubViewMode==='single' && layout?.divisor===1 && !manager?.settings?.fullsize && manager?.settings?.direction==='ltr'){
1531
+ const visible=manager.visible?.();
1532
+ const view=visible?.find(view=>view.index===index);
1533
+ if(view && manager.container){
1534
+ pageGeometry={direction:'ltr',firstIndex:book.spine.first()?.index,lastIndex:book.spine.last()?.index,pageWidth:layout.pageWidth,offset:manager.container.getBoundingClientRect().left-view.position().left};
1535
+ }
1536
+ }
1537
+ epubLocation=window.PapyrusEpubReader.location(location,spineItems.length,epubViewMode==='single'?'paged':'chapter-scroll',pageGeometry);
1500
1538
  if(epubLocation){
1539
+ epubLocation.cfi=epubResizeAnchor?.cfi() || epubLocation.cfi;
1501
1540
  if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
1502
1541
  sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
1503
1542
  }
@@ -1517,6 +1556,8 @@
1517
1556
  );
1518
1557
  if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
1519
1558
  sendEpubDiagnostic('epub.display.end', diagnosticPayload);
1559
+ const resizeRendition=rendition;
1560
+ epubResizeAnchor=window.PapyrusEpubReader.installResizeAnchor(resizeRendition.manager,()=>resizeRendition.location?.start?.cfi);
1520
1561
  if (
1521
1562
  rendition &&
1522
1563
  rendition.manager &&
@@ -1650,6 +1691,7 @@
1650
1691
  const request = ++epubNavigationRequest;
1651
1692
  const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
1652
1693
  if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
1694
+ epubResizeAnchor?.invalidate();
1653
1695
  await activeRendition.display(target);
1654
1696
  if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
1655
1697
  currentPage = pageIndex + 1;
@@ -1839,6 +1881,7 @@
1839
1881
  epubNavigationRequest += 1;
1840
1882
  const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
1841
1883
  if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
1884
+ epubResizeAnchor?.invalidate();
1842
1885
  await activeRendition.display(annotation.anchor.href);
1843
1886
  if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
1844
1887
  const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
@@ -1880,6 +1923,7 @@
1880
1923
  const active = rendition;
1881
1924
  epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
1882
1925
  if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
1926
+ epubResizeAnchor?.invalidate();
1883
1927
  return payload.direction < 0 ? active.prev() : active.next();
1884
1928
  });
1885
1929
  await epubNavigationQueue;
@@ -1892,7 +1936,7 @@
1892
1936
  {
1893
1937
  epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
1894
1938
  if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
1895
- const target=epubLocation?.cfi;
1939
+ const target=epubResizeAnchor?.cfi() || epubLocation?.cfi;
1896
1940
  epubViewMode=next;
1897
1941
  await renderEpubRendition('layout',epubLoadGeneration,target);
1898
1942
  });
@@ -1902,7 +1946,7 @@
1902
1946
  }
1903
1947
  if(kind === 'epub-go-to-location'){
1904
1948
  if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
1905
- epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>payload.documentSessionId===documentSessionId?rendition.display(payload.cfi):undefined);
1949
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>{if(payload.documentSessionId!==documentSessionId)return;epubResizeAnchor?.invalidate();return rendition.display(payload.cfi);});
1906
1950
  await epubNavigationQueue;sendResponse(id,true,null);return;
1907
1951
  }
1908
1952
 
@@ -1913,7 +1957,7 @@
1913
1957
  epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
1914
1958
  if(session!==documentSessionId)return;
1915
1959
  const active=rendition;
1916
- const cfi=epubLocation?.cfi;
1960
+ const cfi=epubResizeAnchor?.cfi() || epubLocation?.cfi;
1917
1961
  zoom=requestedZoom;applyEpubZoom();
1918
1962
  // Let the content CSS update before redisplaying the logical anchor.
1919
1963
  await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));