@papyrus-sdk/ui-react-native 0.2.35-beta.8 → 0.2.35

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.
@@ -20483,6 +20483,191 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20483
20483
  <!-- @papyrus-comic-runtime:end -->
20484
20484
  <script>
20485
20485
  (function () {
20486
+ /* @papyrus-epub-reader:start */
20487
+ (function(root){
20488
+ function mode(value){return value==='chapter-scroll'?'chapter-scroll':'paged';}
20489
+ function validCfi(value){return typeof value==='string' && value.length<=4096 && /^epubcfi\([^\n<>]+\)$/.test(value);}
20490
+ function options(value){return {manager:'default',flow:mode(value)==='paged'?'paginated':'scrolled-doc',spread:'none',width:'100%',height:'100%',allowScriptedContent:false};}
20491
+ function gesture(start,end,state){
20492
+ if(!start || !end || state.selected || state.interactive || state.multitouch)return null;
20493
+ var dx=end.x-start.x,dy=end.y-start.y,elapsed=end.time-start.time;
20494
+ if(state.mode==='paged' && Math.abs(dx)>=48 && Math.abs(dx)>Math.abs(dy)*1.5 && elapsed>=0 && elapsed<1500)return (dx<0)!==Boolean(state.rtl)?'next':'prev';
20495
+ if(elapsed>=0 && elapsed<450 && Math.hypot(dx,dy)<=12)return 'tap';
20496
+ return null;
20497
+ }
20498
+ function location(raw,count,readingMode,geometry){
20499
+ var start=raw && raw.start;if(!start || !validCfi(start.cfi))return null;
20500
+ var displayed=start.displayed||{},chapter=Math.max(1,(Number(start.index)||0)+1);
20501
+ var visualPage=mode(readingMode)==='paged' && Number.isFinite(displayed.page)?displayed.page:null;
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
+ }
20514
+ var fraction=total?Math.max(0,(visualPage-1)/total):0;
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;}};
20528
+ }
20529
+ function install(contents,config){
20530
+ var doc=contents.document,win=contents.window,start=null,blocked=false;
20531
+ function point(e){var t=e.changedTouches&&e.changedTouches[0]||e.touches&&e.touches[0];return t?{x:t.clientX,y:t.clientY,time:Date.now()}:null;}
20532
+ function selected(){return Boolean(String(win.getSelection()||'').trim()) || config.blocked();}
20533
+ function interactive(target){return Boolean(target && target.closest && target.closest('a,button,input,textarea,select,[contenteditable="true"],[data-papyrus-overlay]'));}
20534
+ function begin(e){start=point(e);blocked=e.touches.length!==1 || selected() || interactive(e.target);}
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);}
20538
+ function cancel(){start=null;blocked=true;}
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);};
20542
+ }
20543
+ root.PapyrusEpubReader={mode:mode,validCfi:validCfi,options:options,gesture:gesture,location:location,installResizeAnchor:installResizeAnchor,install:install};
20544
+ })(typeof window==='undefined'?globalThis:window);
20545
+
20546
+ /* @papyrus-epub-reader:end */
20547
+ /* @papyrus-epub-annotations:start */
20548
+ (function (root, factory) {
20549
+ if (typeof module === 'object' && module.exports) module.exports = factory();
20550
+ else root.PapyrusEpubAnnotations = factory();
20551
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
20552
+ function validAnchor(anchor) {
20553
+ return anchor && anchor.version === 1 && anchor.kind === 'epub-cfi' &&
20554
+ typeof anchor.cfiRange === 'string' && anchor.cfiRange.length <= 4096 && /^epubcfi\([^\n<>]+\)$/.test(anchor.cfiRange) &&
20555
+ typeof anchor.href === 'string' && !/^(javascript|data):/i.test(anchor.href) &&
20556
+ typeof anchor.quote === 'string' && anchor.quote.length > 0 && anchor.quote.length <= 100000;
20557
+ }
20558
+ function textNodes(document) {
20559
+ var walker = document.createTreeWalker(document.body, 4);
20560
+ var nodes = [], text = '', node;
20561
+ while ((node = walker.nextNode())) {
20562
+ if (node.parentElement.closest('script,style,[data-papyrus-overlay]')) continue;
20563
+ nodes.push({node:node, start:text.length}); text += node.textContent;
20564
+ }
20565
+ return {nodes:nodes,text:text};
20566
+ }
20567
+ function recoverRange(document, anchor) {
20568
+ var source = textNodes(document), match = null, start = source.text.indexOf(anchor.quote);
20569
+ while (start >= 0) {
20570
+ var end = start + anchor.quote.length;
20571
+ if ((!anchor.prefix || source.text.slice(Math.max(0,start-anchor.prefix.length),start) === anchor.prefix) &&
20572
+ (!anchor.suffix || source.text.slice(end,end+anchor.suffix.length) === anchor.suffix)) {
20573
+ if (match) return null; match = {start:start,end:end};
20574
+ }
20575
+ start = source.text.indexOf(anchor.quote,start+1);
20576
+ }
20577
+ if (!match) return null;
20578
+ var range = document.createRange();
20579
+ var first = source.nodes.find(function(n){return n.start+n.node.length > match.start;});
20580
+ var last = source.nodes.find(function(n){return n.start+n.node.length >= match.end;});
20581
+ if (!first || !last) return null;
20582
+ range.setStart(first.node,match.start-first.start); range.setEnd(last.node,match.end-last.start);
20583
+ return range;
20584
+ }
20585
+ function resolveRange(contents, anchor) {
20586
+ if (!validAnchor(anchor)) return null;
20587
+ try { var range = contents.range(anchor.cfiRange); if (range && range.toString() === anchor.quote) return range; } catch (_) {}
20588
+ return recoverRange(contents.document,anchor);
20589
+ }
20590
+ function selection(contents, cfiRange, href, documentId) {
20591
+ var range;
20592
+ try { range = contents.range(cfiRange); } catch (_) { return null; }
20593
+ if (!range || !range.toString()) return null;
20594
+ var before = contents.document.createRange(); before.selectNodeContents(contents.document.body); before.setEnd(range.startContainer,range.startOffset);
20595
+ var after = contents.document.createRange(); after.selectNodeContents(contents.document.body); after.setStart(range.endContainer,range.endOffset);
20596
+ 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)};
20597
+ return validAnchor(anchor) ? anchor : null;
20598
+ }
20599
+ function install(contents, options) {
20600
+ var doc = contents.document, win = contents.window, alive = true, frame = 0;
20601
+ var overlay = doc.createElement('div'); overlay.dataset.papyrusOverlay = 'annotations';
20602
+ overlay.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:0;pointer-events:none;z-index:3';
20603
+ var originalPosition=doc.body.style.position;
20604
+ if(win.getComputedStyle(doc.body).position==='static')doc.body.style.position='relative';
20605
+ doc.body.appendChild(overlay);
20606
+ var registry = new Map(), hitRegions = [];
20607
+ function render() {
20608
+ frame = 0; if (!alive || !options.isCurrent()) return;
20609
+ var keep = new Set(), noteGroups = new Map();
20610
+ hitRegions = [];
20611
+ options.annotations().forEach(function(annotation) {
20612
+ var anchor = annotation.anchor;
20613
+ if (!validAnchor(anchor) || anchor.href !== options.href || anchor.documentId && options.documentId && anchor.documentId !== options.documentId) return;
20614
+ var range = resolveRange(contents,anchor); if (!range) return;
20615
+ var rects = Array.from(range.getClientRects()).filter(function(r){return r.width>0 && r.height>0;});
20616
+ if (!rects.length) return;
20617
+ keep.add(annotation.id);
20618
+ var group = registry.get(annotation.id);
20619
+ 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'}); } }
20620
+ group.replaceChildren();
20621
+ hitRegions.push({id:annotation.id,rects:rects});
20622
+ var color = /^#[0-9a-f]{6}$/i.test(annotation.color) ? annotation.color : '#FFC928';
20623
+ var style = annotation.markupStyle || annotation.type;
20624
+ var opacity = Number.isFinite(annotation.opacity) ? Math.max(0,Math.min(1,annotation.opacity)) : 0.35;
20625
+ if(style === 'highlight')opacity=Math.min(0.35,opacity);
20626
+ rects.forEach(function(rect) {
20627
+ var mark = doc.createElement('div');
20628
+ var bodyRect=doc.body.getBoundingClientRect();
20629
+ var height = style === 'highlight' ? rect.height : 2;
20630
+ var y = style === 'strikeout' ? rect.top+rect.height/2 : style === 'underline' ? rect.bottom-2 : rect.top;
20631
+ 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;
20632
+ if (['highlight','underline','strikeout'].includes(style)) group.appendChild(mark);
20633
+ });
20634
+ var hasNote = annotation.noteContent !== undefined || annotation.type === 'comment' || annotation.type === 'text';
20635
+ if (hasNote) {
20636
+ var last = rects[rects.length-1], button = doc.createElement('button');
20637
+ button.textContent = '●'; button.setAttribute('aria-label',options.labels().comment || '');
20638
+ var pageWidth=Math.max(44, options.pageWidth ? options.pageWidth() : win.innerWidth);
20639
+ var bodyRect=doc.body.getBoundingClientRect(), column=Math.max(0,Math.floor((last.left-bodyRect.left)/pageWidth));
20640
+ button.style.cssText = 'position:absolute;pointer-events:auto;border:0;background:transparent;color:'+color+';width:44px;height:44px;left:'+((column+1)*pageWidth-44)+'px;top:'+(Math.floor(last.top/44)*44-doc.body.getBoundingClientRect().top)+'px';
20641
+ var key = column+":"+Math.floor(last.top/44);
20642
+ var neighbors=noteGroups.get(key);
20643
+ if(!neighbors){neighbors={button:button,ids:[]};noteGroups.set(key,neighbors);group.appendChild(button);}
20644
+ neighbors.ids.push(annotation.id);
20645
+ }
20646
+ });
20647
+ noteGroups.forEach(function(group){
20648
+ group.button.textContent=group.ids.length>1?String(group.ids.length):'●';
20649
+ group.button.addEventListener('click',function(e){e.stopPropagation();if(options.isCurrent())options.onTap(group.ids[0],group.ids);});
20650
+ });
20651
+ registry.forEach(function(group,id){if (!keep.has(id)) {group.remove();registry.delete(id);}});
20652
+ }
20653
+ function schedule() { if (alive && !frame) frame = win.requestAnimationFrame(render); }
20654
+ function tapMarkup(event) {
20655
+ if(!alive || !options.isCurrent() || String(win.getSelection() || '').length || event.target.closest('[data-papyrus-overlay]'))return;
20656
+ 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;});});
20657
+ if(hit) {event.stopPropagation();options.onTap(hit.id);}
20658
+ }
20659
+ doc.addEventListener('click',tapMarkup);
20660
+ var observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;
20661
+ if (observer) observer.observe(doc.body);
20662
+ win.addEventListener('resize',schedule);win.addEventListener('scroll',schedule); doc.addEventListener('load',schedule,true);
20663
+ if (doc.fonts && doc.fonts.ready) doc.fonts.ready.then(schedule);
20664
+ schedule();
20665
+ return {schedule:schedule, hit:function(x,y){return hitRegions.some(function(item){return item.rects.some(function(r){return x>=r.left && x<=r.right && y>=r.top && y<=r.bottom;});});}, range:function(anchor){return resolveRange(contents,anchor);}, destroy:function(){alive=false;if(frame)win.cancelAnimationFrame(frame);if(observer)observer.disconnect();win.removeEventListener('resize',schedule);win.removeEventListener('scroll',schedule);doc.removeEventListener('load',schedule,true);doc.removeEventListener('click',tapMarkup);overlay.remove();doc.body.style.position=originalPosition;registry.clear();}};
20666
+ }
20667
+ return {validAnchor:validAnchor,recoverRange:recoverRange,resolveRange:resolveRange,selection:selection,install:install};
20668
+ });
20669
+
20670
+ /* @papyrus-epub-annotations:end */
20486
20671
  const viewer = document.getElementById('viewer');
20487
20672
  const DEFAULT_FONT_SIZE = 16;
20488
20673
  const TEXT_PAGE_CHUNK = 1600;
@@ -20490,6 +20675,12 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20490
20675
  let currentType = null;
20491
20676
  let book = null;
20492
20677
  let rendition = null;
20678
+ let epubResizeAnchor = null;
20679
+ let documentSessionId = null;
20680
+ let annotationDocumentId;
20681
+ let annotationLabels = {};
20682
+ let epubAnnotations = [];
20683
+ const epubAnnotationRenderers = new Map();
20493
20684
  let spineItems = [];
20494
20685
  let textPages = [];
20495
20686
  let textContainer = null;
@@ -20897,6 +21088,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
20897
21088
  };
20898
21089
 
20899
21090
  const clearViewer = () => {
21091
+ if(epubResizeAnchor){epubResizeAnchor.destroy();epubResizeAnchor=null;}
21092
+ epubAnnotationRenderers.forEach(renderer => renderer.destroy());
21093
+ epubAnnotationRenderers.clear();
20900
21094
  if (epubInteractionCleanup) {
20901
21095
  epubInteractionCleanup();
20902
21096
  epubInteractionCleanup = null;
@@ -21551,21 +21745,65 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21551
21745
  pageCount,
21552
21746
  });
21553
21747
 
21748
+ if (book.locations?.generate) {
21749
+ const activeBook=book;
21750
+ activeBook.locations.generate(150).then(() => {
21751
+ if (isCurrentLoad() && activeBook===book && epubLocation) {
21752
+ epubLocation.progress=activeBook.locations.percentageFromCfi(epubLocation.cfi);
21753
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
21754
+ }
21755
+ }).catch(() => {});
21756
+ }
21757
+ return renderEpubRendition(loadId,generation,null);
21758
+ };
21759
+
21760
+ const renderEpubRendition = async (loadId,generation,target) => {
21761
+ const diagnosticPayload={loadId};
21762
+ const isCurrentLoad=()=>generation===epubLoadGeneration;
21763
+ if(epubInteractionCleanup)epubInteractionCleanup();
21764
+ if(rendition)rendition.destroy();
21554
21765
  clearViewer();
21555
- rendition = book.renderTo(viewer, {
21556
- manager: 'continuous',
21557
- width: '100%',
21558
- height: '100%',
21559
- flow: 'scrolled-continuous',
21560
- spread: 'none',
21561
- });
21766
+ rendition = book.renderTo(viewer, window.PapyrusEpubReader.options(epubViewMode === 'single' ? 'paged' : 'chapter-scroll'));
21562
21767
  sendEpubDiagnostic('epub.rendition.created', diagnosticPayload);
21563
21768
 
21564
21769
  if (rendition && rendition.hooks && rendition.hooks.content) {
21770
+ const session = documentSessionId;
21771
+ const activeRendition = rendition;
21565
21772
  rendition.hooks.content.register((contents) => {
21773
+ const removeGestureListeners = window.PapyrusEpubReader.install(contents, {
21774
+ mode: () => epubViewMode === 'single' ? 'paged' : 'chapter-scroll',
21775
+ rtl: () => book.package?.metadata?.direction === 'rtl' || book.spine?.direction === 'rtl',
21776
+ blocked: () => Boolean(nativeMenuSelection && String(nativeMenuSelection.contents?.window?.getSelection() || '').trim()),
21777
+ annotationHit: point => epubAnnotationRenderers.get(contents)?.hit(point.x,point.y),
21778
+ current: () => session === documentSessionId && activeRendition === rendition,
21779
+ interact: () => epubResizeAnchor?.invalidate(),
21780
+ tap: () => sendEvent('EPUB_CONTENT_TAP',{documentSessionId:session}),
21781
+ turn: direction => {
21782
+ epubNavigationQueue = epubNavigationQueue.catch(()=>{}).then(() => {
21783
+ if(session !== documentSessionId || activeRendition !== rendition)return;
21784
+ epubResizeAnchor?.invalidate();
21785
+ return direction<0?activeRendition.prev():activeRendition.next();
21786
+ });
21787
+ epubNavigationQueue.catch(error => sendEvent('EPUB_NAVIGATION_ERROR',{message:String(error)}));
21788
+ },
21789
+ });
21790
+ const gestureCleanup = () => {removeGestureListeners();epubSelectionCleanups.delete(gestureCleanup);};
21791
+ epubSelectionCleanups.add(gestureCleanup);
21792
+ contents.__papyrusGestureCleanup=gestureCleanup;
21793
+ const href = spineItems[contents.sectionIndex]?.href;
21794
+ if (href && window.PapyrusEpubAnnotations) {
21795
+ const renderer = window.PapyrusEpubAnnotations.install(contents, {
21796
+ href, get documentId() { return annotationDocumentId; },
21797
+ isCurrent: () => session === documentSessionId && activeRendition === rendition,
21798
+ pageWidth: () => activeRendition.layout?.width || viewer.clientWidth,
21799
+ annotations: () => epubAnnotations, labels: () => annotationLabels,
21800
+ onTap: (id, ids) => sendEvent("EPUB_ANNOTATION_TAP", {documentSessionId: session, id, ids}),
21801
+ });
21802
+ epubAnnotationRenderers.set(contents,renderer);
21803
+ }
21566
21804
  const frame = contents && contents.document ? contents.document.defaultView.frameElement : null;
21567
21805
  if (frame) {
21568
- frame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
21806
+ frame.setAttribute('sandbox', 'allow-same-origin');
21569
21807
  }
21570
21808
  const selectionDocument = contents && contents.document;
21571
21809
  const selectionWindow = contents && contents.window;
@@ -21575,13 +21813,42 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21575
21813
  selectionWindow &&
21576
21814
  typeof selectionWindow.getSelection === 'function'
21577
21815
  ) {
21816
+ let selectionTimer = null;
21817
+
21818
+ const reportSelection = () => {
21819
+ selectionTimer = null;
21820
+ if (session !== documentSessionId || activeRendition !== rendition) return;
21821
+ const selected = selectionWindow.getSelection();
21822
+ if (!selected || selected.isCollapsed || selected.rangeCount === 0 ||
21823
+ typeof contents.cfiFromRange !== 'function') return;
21824
+ const range = selected.getRangeAt(0).cloneRange();
21825
+ if (!range.toString().trim()) return;
21826
+ const href = spineItems[contents.sectionIndex]?.href;
21827
+ if (!href) return;
21828
+ let cfi;
21829
+ try { cfi = contents.cfiFromRange(range); } catch (_) { return; }
21830
+ const anchor = window.PapyrusEpubAnnotations?.selection(contents, cfi, href, annotationDocumentId);
21831
+ if (!anchor) return;
21832
+ if (nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
21833
+ const payload = {documentSessionId: session, text: anchor.quote,
21834
+ pageIndex: contents.sectionIndex + 1, anchor};
21835
+ nativeMenuSelection = {contents, payload};
21836
+ sendEvent('EPUB_TEXT_SELECTED', payload);
21837
+ };
21578
21838
  const handleSelectionChange = () => {
21579
21839
  const selection = selectionWindow.getSelection();
21580
21840
  const text = selection ? selection.toString().trim() : '';
21581
21841
  epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
21842
+ if (selectionTimer !== null) clearTimeout(selectionTimer);
21843
+ // epub.js "selected" is not reliable on every iOS WKWebView path.
21844
+ // The content document still emits selectionchange, so resolve its
21845
+ // live Range to a CFI before the native menu action can fire.
21846
+ if (text) reportSelection();
21847
+ else if (nativeMenuSelection?.contents === contents) nativeMenuSelection = null;
21582
21848
  };
21583
21849
  selectionDocument.addEventListener('selectionchange', handleSelectionChange);
21584
21850
  const selectionCleanup = () => {
21851
+ if (selectionTimer !== null) clearTimeout(selectionTimer);
21585
21852
  selectionDocument.removeEventListener('selectionchange', handleSelectionChange);
21586
21853
  epubSelectionCleanups.delete(selectionCleanup);
21587
21854
  };
@@ -21600,29 +21867,53 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21600
21867
  typeof rendition.hooks.unloaded.register === 'function'
21601
21868
  ) {
21602
21869
  rendition.hooks.unloaded.register((view) => {
21870
+ view?.contents?.__papyrusGestureCleanup?.();
21603
21871
  const cleanup = view && view.contents && view.contents.__papyrusSelectionCleanup;
21604
21872
  if (typeof cleanup === 'function') cleanup();
21873
+ const renderer = epubAnnotationRenderers.get(view?.contents);
21874
+ if (renderer) renderer.destroy();
21875
+ epubAnnotationRenderers.delete(view?.contents);
21605
21876
  });
21606
21877
  }
21607
21878
 
21608
21879
  if (rendition && typeof rendition.on === 'function') {
21880
+ const selectionSession = documentSessionId;
21881
+ const selectionRendition = rendition;
21609
21882
  rendition.on('selected', (cfiRange, contents) => {
21610
- const selection = contents && contents.window ? contents.window.getSelection() : null;
21611
- const text = selection ? selection.toString().trim() : '';
21612
- epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(Boolean(text));
21613
- if (text) {
21614
- sendEvent('TEXT_SELECTED', { text, pageIndex: Math.max(0, currentPage - 1) });
21615
- }
21616
- if (rendition && rendition.annotations && typeof rendition.annotations.remove === 'function') {
21617
- rendition.annotations.remove(cfiRange, 'highlight');
21618
- }
21883
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21884
+ const href = spineItems[contents.sectionIndex]?.href;
21885
+ const anchor = href && window.PapyrusEpubAnnotations?.selection(contents,cfiRange,href,annotationDocumentId);
21886
+ if (!anchor || nativeMenuSelection?.payload?.anchor?.cfiRange === anchor.cfiRange && nativeMenuSelection?.payload?.anchor?.quote === anchor.quote) return;
21887
+ epubScrollDiagnostics && epubScrollDiagnostics.setSelectionActive(true);
21888
+ const payload = {documentSessionId: selectionSession, text: anchor.quote, pageIndex: contents.sectionIndex+1, anchor};
21889
+ nativeMenuSelection = {contents, payload};
21890
+ sendEvent('EPUB_TEXT_SELECTED', payload);
21619
21891
  });
21892
+ ['rendered','resized','layout','relocated'].forEach(name => rendition.on(name, () => {
21893
+ if (selectionSession === documentSessionId && selectionRendition === rendition) epubAnnotationRenderers.forEach(renderer => renderer.schedule());
21894
+ }));
21620
21895
  rendition.on('relocated', (location) => {
21896
+ if (selectionSession !== documentSessionId || selectionRendition !== rendition) return;
21621
21897
  const start = location && location.start;
21622
21898
  let index = start && Number.isInteger(start.index) ? start.index : -1;
21623
21899
  if (index < 0 && start && start.href) index = getSpineIndexByHref(start.href);
21624
21900
  if (index < 0 || index >= spineItems.length) return;
21625
21901
  epubScrollDiagnostics && epubScrollDiagnostics.relocated(location);
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);
21912
+ if(epubLocation){
21913
+ epubLocation.cfi=epubResizeAnchor?.cfi() || epubLocation.cfi;
21914
+ if(book.locations?.length?.()>0)epubLocation.progress=book.locations.percentageFromCfi(epubLocation.cfi);
21915
+ sendEvent('EPUB_LOCATION',{documentSessionId,location:epubLocation});
21916
+ }
21626
21917
  const nextPage = index + 1;
21627
21918
  if (nextPage === currentPage) return;
21628
21919
  currentPage = nextPage;
@@ -21630,21 +21921,23 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21630
21921
  });
21631
21922
  }
21632
21923
 
21924
+ applyEpubZoom();
21633
21925
  sendEpubDiagnostic('epub.display.start', diagnosticPayload);
21634
21926
  await withEpubStageTimeout(
21635
- displayEpubPage(0),
21927
+ rendition.display(target || spineItems[0]?.href),
21636
21928
  'rendition.display',
21637
21929
  diagnosticPayload,
21638
21930
  );
21639
21931
  if (!isCurrentLoad()) throw new Error('EPUB load superseded by a newer load');
21640
21932
  sendEpubDiagnostic('epub.display.end', diagnosticPayload);
21933
+ const resizeRendition=rendition;
21934
+ epubResizeAnchor=window.PapyrusEpubReader.installResizeAnchor(resizeRendition.manager,()=>resizeRendition.location?.start?.cfi);
21641
21935
  if (
21642
21936
  rendition &&
21643
21937
  rendition.manager &&
21644
21938
  typeof rendition.manager.on === 'function' &&
21645
21939
  typeof rendition.on === 'function'
21646
21940
  ) {
21647
- let touchStart = null;
21648
21941
  let lastScrollOffset = -1;
21649
21942
  const manager = rendition.manager;
21650
21943
  epubScrollDiagnostics = createEpubScrollDiagnostics(manager);
@@ -21719,47 +22012,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21719
22012
  return result;
21720
22013
  };
21721
22014
  }
21722
- const getTouchPoint = (event) => {
21723
- const touch =
21724
- (event && event.changedTouches && event.changedTouches[0]) ||
21725
- (event && event.touches && event.touches[0]);
21726
- if (!touch) return null;
21727
- const x = Number(touch.screenX ?? touch.clientX);
21728
- const y = Number(touch.screenY ?? touch.clientY);
21729
- return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
21730
- };
21731
- const handleScroll = (event) => {
22015
+ const handleScroll = event => {
21732
22016
  const offsetY = Math.max(0, Number(event && event.top) || 0);
21733
22017
  epubScrollDiagnostics && epubScrollDiagnostics.scroll(event);
21734
- if (Math.abs(offsetY - lastScrollOffset) < 1) return;
21735
- lastScrollOffset = offsetY;
21736
- sendEvent('VIEWER_SCROLL', { offsetY });
21737
- };
21738
- const handleTouchStart = (event) => {
21739
- touchStart = getTouchPoint(event);
21740
- epubScrollDiagnostics && epubScrollDiagnostics.touchStart(event);
21741
- };
21742
- const handleTouchMove = (event) => {
21743
- epubScrollDiagnostics && epubScrollDiagnostics.touchMove(event);
21744
- };
21745
- const handleTouchEnd = (event) => {
21746
- const end = getTouchPoint(event);
21747
- const start = touchStart;
21748
- touchStart = null;
21749
- if (start && end && Math.hypot(end.x - start.x, end.y - start.y) <= 12) {
21750
- sendEvent('VIEWER_TAP', {});
21751
- }
21752
- epubScrollDiagnostics && epubScrollDiagnostics.touchEnd(event);
22018
+ if (Math.abs(offsetY-lastScrollOffset)<1) return;
22019
+ lastScrollOffset=offsetY;
22020
+ sendEvent('VIEWER_SCROLL',{offsetY});
21753
22021
  };
21754
-
21755
22022
  manager.on('scroll', handleScroll);
21756
22023
  const handleScrolled = (event) => {
21757
22024
  epubScrollDiagnostics && epubScrollDiagnostics.settled(event);
21758
22025
  };
21759
22026
  manager.on('scrolled', handleScrolled);
21760
- rendition.on('touchstart', handleTouchStart);
21761
- rendition.on('touchmove', handleTouchMove);
21762
- rendition.on('touchend', handleTouchEnd);
22027
+
21763
22028
  epubInteractionCleanup = () => {
21764
22029
  epubSelectionCleanups.forEach((cleanup) => cleanup());
21765
22030
  epubSelectionCleanups.clear();
@@ -21768,16 +22033,9 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21768
22033
  if (originalEnqueue) queue.enqueue = originalEnqueue;
21769
22034
  if (typeof manager.off === 'function') manager.off('scroll', handleScroll);
21770
22035
  if (typeof manager.off === 'function') manager.off('scrolled', handleScrolled);
21771
- if (typeof rendition.off === 'function') {
21772
- rendition.off('touchstart', handleTouchStart);
21773
- rendition.off('touchmove', handleTouchMove);
21774
- rendition.off('touchend', handleTouchEnd);
21775
- }
21776
22036
  epubScrollDiagnostics = null;
21777
22037
  };
21778
22038
  }
21779
- applyEpubZoom();
21780
-
21781
22039
  const outline = await withEpubStageTimeout(
21782
22040
  buildOutline(),
21783
22041
  'outline',
@@ -21791,6 +22049,8 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21791
22049
  return { pageCount, outline };
21792
22050
  };
21793
22051
 
22052
+ let epubNavigationQueue = Promise.resolve();
22053
+ let epubNavigationRequest = 0;
21794
22054
  const displayEpubPage = async (pageIndex) => {
21795
22055
  if (!rendition) return;
21796
22056
  const item = spineItems[pageIndex];
@@ -21800,9 +22060,19 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21800
22060
  target,
21801
22061
  pageIndex,
21802
22062
  });
21803
- await rendition.display(target);
21804
- currentPage = pageIndex + 1;
21805
- sendState();
22063
+ const activeRendition = rendition;
22064
+ const session = documentSessionId;
22065
+ const request = ++epubNavigationRequest;
22066
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22067
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22068
+ epubResizeAnchor?.invalidate();
22069
+ await activeRendition.display(target);
22070
+ if (request !== epubNavigationRequest || session !== documentSessionId || activeRendition !== rendition) return;
22071
+ currentPage = pageIndex + 1;
22072
+ sendState();
22073
+ });
22074
+ epubNavigationQueue = navigation;
22075
+ await navigation;
21806
22076
  };
21807
22077
 
21808
22078
  const getTextContent = async (pageIndex) => {
@@ -21903,6 +22173,10 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21903
22173
  }
21904
22174
  };
21905
22175
 
22176
+ let nativeMenuSelection = null;
22177
+ let epubViewMode = "single";
22178
+ let epubLocation = null;
22179
+
21906
22180
  const handleCommand = async (message) => {
21907
22181
  const { id, kind, payload } = message;
21908
22182
 
@@ -21910,7 +22184,15 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21910
22184
  if (kind === 'load') {
21911
22185
  const generation = (epubLoadGeneration += 1);
21912
22186
  currentType = payload.type;
22187
+ documentSessionId = payload.documentSessionId || null;
22188
+ epubAnnotations = [];
22189
+ annotationDocumentId = undefined;
21913
22190
  zoom = 1.0;
22191
+ epubViewMode = payload.epubMode === "chapter-scroll" ? "continuous" : "single";
22192
+ epubLocation = null;
22193
+ nativeMenuSelection = null;
22194
+ epubNavigationRequest += 1;
22195
+ epubNavigationQueue = Promise.resolve();
21914
22196
 
21915
22197
  if (currentType === 'text') {
21916
22198
  const result = await loadText(
@@ -21950,6 +22232,43 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21950
22232
  throw new Error('Unsupported document type');
21951
22233
  }
21952
22234
 
22235
+ if (kind === 'epub-annotations' || kind === 'epub-annotation-navigate') {
22236
+ if (currentType !== 'epub' || !documentSessionId || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB annotation session');
22237
+ if (kind === 'epub-annotations') {
22238
+ const incoming = payload.annotations;
22239
+ 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');
22240
+ const ids = new Set();
22241
+ epubAnnotations = incoming.filter(a => {
22242
+ if (ids.has(a.id) || !window.PapyrusEpubAnnotations.validAnchor(a.anchor)) return false;
22243
+ ids.add(a.id); return true;
22244
+ });
22245
+ annotationDocumentId = typeof payload.documentId === 'string' ? payload.documentId : undefined;
22246
+ annotationLabels = {comment: typeof payload.labels?.comment === 'string' ? payload.labels.comment.slice(0,128) : ''};
22247
+ epubAnnotationRenderers.forEach(renderer => renderer.schedule());
22248
+ } else {
22249
+ const annotation = epubAnnotations.find(a => a.id === payload.annotationId);
22250
+ if (!annotation || (annotation.anchor.documentId && annotation.anchor.documentId !== annotationDocumentId)) throw new Error('Annotation not available for this document');
22251
+ // Navigation uses the CFI, never a synthetic percentage/page rectangle.
22252
+ const navigationSession = documentSessionId;
22253
+ const activeRendition = rendition;
22254
+ // Open the confirmed chapter first, resolve its quote, then navigate its actual range.
22255
+ epubNavigationRequest += 1;
22256
+ const navigation = epubNavigationQueue.catch(() => {}).then(async () => {
22257
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22258
+ epubResizeAnchor?.invalidate();
22259
+ await activeRendition.display(annotation.anchor.href);
22260
+ if(navigationSession !== documentSessionId || activeRendition !== rendition)throw new Error('Stale EPUB navigation');
22261
+ const contents = activeRendition.getContents().find(c => spineItems[c.sectionIndex]?.href === annotation.anchor.href);
22262
+ const range = contents && window.PapyrusEpubAnnotations.resolveRange(contents,annotation.anchor);
22263
+ if(!range)throw new Error('EPUB anchor unresolved');
22264
+ await activeRendition.display(contents.cfiFromRange(range));
22265
+ });
22266
+ epubNavigationQueue = navigation;
22267
+ await navigation;
22268
+ }
22269
+ sendResponse(id,true,null); return;
22270
+ }
22271
+
21953
22272
  if (kind === 'go-to-page') {
21954
22273
  const page = Math.max(1, payload.page || 1);
21955
22274
  if (currentType === 'text') {
@@ -21972,12 +22291,55 @@ https://github.com/nodeca/pako/blob/main/LICENSE
21972
22291
  return;
21973
22292
  }
21974
22293
 
22294
+ if (kind === 'epub-turn-page') {
22295
+ if (epubViewMode !== 'single') {sendResponse(id,true,null); return;}
22296
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB navigation');
22297
+ const active = rendition;
22298
+ epubNavigationQueue = epubNavigationQueue.catch(() => {}).then(() => {
22299
+ if (active !== rendition || payload.documentSessionId !== documentSessionId) return;
22300
+ epubResizeAnchor?.invalidate();
22301
+ return payload.direction < 0 ? active.prev() : active.next();
22302
+ });
22303
+ await epubNavigationQueue;
22304
+ sendResponse(id,true,null); return;
22305
+ }
22306
+
22307
+ if (kind === 'set-epub-view-mode') {
22308
+ if (currentType !== 'epub' || payload.documentSessionId !== documentSessionId) throw new Error('Stale EPUB layout session');
22309
+ const next=payload.epubMode ? (payload.epubMode==='paged'?'single':'continuous') : (payload.viewMode==='single'?'single':'continuous');
22310
+ {
22311
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22312
+ if(payload.documentSessionId!==documentSessionId || next===epubViewMode)return;
22313
+ const target=epubResizeAnchor?.cfi() || epubLocation?.cfi;
22314
+ epubViewMode=next;
22315
+ await renderEpubRendition('layout',epubLoadGeneration,target);
22316
+ });
22317
+ await epubNavigationQueue;
22318
+ }
22319
+ sendResponse(id,true,null);return;
22320
+ }
22321
+ if(kind === 'epub-go-to-location'){
22322
+ if(currentType!=='epub' || payload.documentSessionId!==documentSessionId || !window.PapyrusEpubReader.validCfi(payload.cfi))throw new Error('Invalid EPUB location');
22323
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(()=>{if(payload.documentSessionId!==documentSessionId)return;epubResizeAnchor?.invalidate();return rendition.display(payload.cfi);});
22324
+ await epubNavigationQueue;sendResponse(id,true,null);return;
22325
+ }
22326
+
21975
22327
  if (kind === 'set-zoom') {
21976
- zoom = Math.max(0.5, Math.min(4.0, payload.zoom || 1.0));
21977
- applyZoom();
21978
- sendState();
21979
- sendResponse(id, true, { zoom });
21980
- return;
22328
+ const requestedZoom=Math.max(0.5,Math.min(4.0,payload.zoom || 1.0));
22329
+ if(currentType === 'epub') {
22330
+ const session=documentSessionId;
22331
+ epubNavigationQueue=epubNavigationQueue.catch(()=>{}).then(async()=>{
22332
+ if(session!==documentSessionId)return;
22333
+ const active=rendition;
22334
+ const cfi=epubResizeAnchor?.cfi() || epubLocation?.cfi;
22335
+ zoom=requestedZoom;applyEpubZoom();
22336
+ // Let the content CSS update before redisplaying the logical anchor.
22337
+ await new Promise(resolve=>(window.requestAnimationFrame || (callback=>setTimeout(callback,0)))(resolve));
22338
+ if(cfi && active===rendition && session===documentSessionId)await active.display(cfi);
22339
+ });
22340
+ await epubNavigationQueue;
22341
+ } else {zoom=requestedZoom;applyZoom();}
22342
+ sendState();sendResponse(id,true,{zoom});return;
21981
22343
  }
21982
22344
 
21983
22345
  if (kind === 'set-rotation') {
@@ -22119,6 +22481,26 @@ https://github.com/nodeca/pako/blob/main/LICENSE
22119
22481
  return;
22120
22482
  }
22121
22483
 
22484
+ if (message.type === 'epub-selection-action') {
22485
+ const selected = nativeMenuSelection;
22486
+ if (!selected || message.documentSessionId !== documentSessionId ||
22487
+ selected.payload.documentSessionId !== documentSessionId) return;
22488
+ if (message.key === 'selectAll') {
22489
+ const doc = selected.contents.document;
22490
+ const range = doc.createRange();
22491
+ range.selectNodeContents(doc.body);
22492
+ const selection = selected.contents.window.getSelection();
22493
+ selection.removeAllRanges();
22494
+ selection.addRange(range);
22495
+ return;
22496
+ }
22497
+ if (!['copy','define','highlight','underline','strikeout','comment'].includes(message.key)) return;
22498
+ sendEvent('EPUB_SELECTION_ACTION', {...selected.payload, action: message.key});
22499
+ selected.contents.window.getSelection()?.removeAllRanges();
22500
+ nativeMenuSelection = null;
22501
+ return;
22502
+ }
22503
+
22122
22504
  if (!message.kind) return;
22123
22505
  handleCommand(message);
22124
22506
  };