@embedpdf/plugin-selection 2.14.4 → 3.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +192 -21
- package/dist/index.cjs +575 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +167 -0
- package/dist/index.d.ts +167 -1
- package/dist/index.js +549 -1375
- package/dist/index.js.map +1 -1
- package/package.json +21 -62
- package/dist/lib/actions.d.ts +0 -103
- package/dist/lib/handlers/index.d.ts +0 -2
- package/dist/lib/handlers/marquee-selection.handler.d.ts +0 -30
- package/dist/lib/handlers/text-selection.handler.d.ts +0 -52
- package/dist/lib/index.d.ts +0 -9
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/reducer.d.ts +0 -5
- package/dist/lib/selection-plugin.d.ts +0 -92
- package/dist/lib/selectors.d.ts +0 -14
- package/dist/lib/types.d.ts +0 -266
- package/dist/lib/utils.d.ts +0 -108
- package/dist/preact/adapter.d.ts +0 -4
- package/dist/preact/core.d.ts +0 -1
- package/dist/preact/index.cjs +0 -2
- package/dist/preact/index.cjs.map +0 -1
- package/dist/preact/index.d.ts +0 -1
- package/dist/preact/index.js +0 -218
- package/dist/preact/index.js.map +0 -1
- package/dist/preact/interaction-manager.d.ts +0 -1
- package/dist/preact/utils.d.ts +0 -1
- package/dist/react/adapter.d.ts +0 -2
- package/dist/react/core.d.ts +0 -1
- package/dist/react/index.cjs +0 -2
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -217
- package/dist/react/index.js.map +0 -1
- package/dist/react/interaction-manager.d.ts +0 -1
- package/dist/react/utils.d.ts +0 -1
- package/dist/shared/components/copy-to-clipboard.d.ts +0 -1
- package/dist/shared/components/index.d.ts +0 -4
- package/dist/shared/components/marquee-selection.d.ts +0 -36
- package/dist/shared/components/selection-layer.d.ts +0 -29
- package/dist/shared/components/text-selection.d.ts +0 -21
- package/dist/shared/hooks/index.d.ts +0 -1
- package/dist/shared/hooks/use-selection.d.ts +0 -11
- package/dist/shared/index.d.ts +0 -5
- package/dist/shared/types.d.ts +0 -7
- package/dist/shared-preact/components/copy-to-clipboard.d.ts +0 -1
- package/dist/shared-preact/components/index.d.ts +0 -4
- package/dist/shared-preact/components/marquee-selection.d.ts +0 -36
- package/dist/shared-preact/components/selection-layer.d.ts +0 -29
- package/dist/shared-preact/components/text-selection.d.ts +0 -21
- package/dist/shared-preact/hooks/index.d.ts +0 -1
- package/dist/shared-preact/hooks/use-selection.d.ts +0 -11
- package/dist/shared-preact/index.d.ts +0 -5
- package/dist/shared-preact/types.d.ts +0 -7
- package/dist/shared-react/components/copy-to-clipboard.d.ts +0 -1
- package/dist/shared-react/components/index.d.ts +0 -4
- package/dist/shared-react/components/marquee-selection.d.ts +0 -36
- package/dist/shared-react/components/selection-layer.d.ts +0 -29
- package/dist/shared-react/components/text-selection.d.ts +0 -21
- package/dist/shared-react/hooks/index.d.ts +0 -1
- package/dist/shared-react/hooks/use-selection.d.ts +0 -11
- package/dist/shared-react/index.d.ts +0 -5
- package/dist/shared-react/types.d.ts +0 -7
- package/dist/svelte/components/CopyToClipboard.svelte.d.ts +0 -3
- package/dist/svelte/components/MarqueeSelection.svelte.d.ts +0 -27
- package/dist/svelte/components/SelectionLayer.svelte.d.ts +0 -32
- package/dist/svelte/components/TextSelection.svelte.d.ts +0 -22
- package/dist/svelte/components/index.d.ts +0 -4
- package/dist/svelte/hooks/index.d.ts +0 -1
- package/dist/svelte/hooks/use-selection.svelte.d.ts +0 -19
- package/dist/svelte/index.cjs +0 -2
- package/dist/svelte/index.cjs.map +0 -1
- package/dist/svelte/index.d.ts +0 -5
- package/dist/svelte/index.js +0 -359
- package/dist/svelte/index.js.map +0 -1
- package/dist/svelte/types.d.ts +0 -7
- package/dist/vue/components/copy-to-clipboard.vue.d.ts +0 -3
- package/dist/vue/components/index.d.ts +0 -4
- package/dist/vue/components/marquee-selection.vue.d.ts +0 -29
- package/dist/vue/components/selection-layer.vue.d.ts +0 -60
- package/dist/vue/components/text-selection.vue.d.ts +0 -51
- package/dist/vue/hooks/index.d.ts +0 -1
- package/dist/vue/hooks/use-selection.d.ts +0 -11
- package/dist/vue/index.cjs +0 -2
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.ts +0 -5
- package/dist/vue/index.js +0 -311
- package/dist/vue/index.js.map +0 -1
- package/dist/vue/types.d.ts +0 -7
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,575 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("@embedpdf/core"),t=require("@embedpdf/models"),n="selection",i={id:n,name:"Selection Plugin",version:"1.0.0",provides:["selection"],requires:["interaction-manager"],optional:["viewport","scroll"],defaultConfig:{menuHeight:40}},o="SELECTION/INIT_STATE",s="SELECTION/CLEANUP_STATE",r="SELECTION/CACHE_PAGE_GEOMETRY",c="SELECTION/SET_SELECTION",a="SELECTION/START_SELECTION",l="SELECTION/END_SELECTION",d="SELECTION/CLEAR_SELECTION",h="SELECTION/SET_RECTS",u="SELECTION/SET_SLICES",g="SELECTION/EVICT_PAGE_GEOMETRY",p=(e,t)=>({type:c,payload:{documentId:e,selection:t}}),m=e=>({type:a,payload:{documentId:e}}),f=e=>({type:l,payload:{documentId:e}}),S=(e,t)=>({type:h,payload:{documentId:e,rects:t}}),y=(e,t)=>({type:u,payload:{documentId:e,slices:t}}),x={geometry:{},rects:{},slices:{},selection:null,active:!1,selecting:!1},C={documents:{}},b=(e,t,n)=>({...e,documents:{...e.documents,[t]:n}});function M(e,t){return e.rects[t]??[]}function I(e,n){return t.boundingRect(M(e,n))}function E(e){const n=[],i=e.rects;for(const o in i){const e=Number(o),s=t.boundingRect(i[e]);s&&n.push({page:e,rect:s})}return n}function P(e,t){const n=e.rects[t]||[];if(0===n.length)return null;const i=I(e,t);return i?{pageIndex:t,rect:i,segmentRects:n}:null}function v(e){const t=[],n=Object.keys(e.rects).map(Number);for(const i of n){const n=e.rects[i]||[];if(0===n.length)continue;const o=I(e,i);o&&t.push({pageIndex:i,rect:o,segmentRects:n})}return t}function T(e,t,n=1.5){for(const c of e.runs){if(!(t.y>=c.rect.y&&t.y<=c.rect.y+c.rect.height&&t.x>=c.rect.x&&t.x<=c.rect.x+c.rect.width))continue;const e=c.glyphs.findIndex(e=>{const n=e.tightX??e.x,i=e.tightY??e.y,o=e.tightWidth??e.width,s=e.tightHeight??e.height;return t.x>=n&&t.x<=n+o&&t.y>=i&&t.y<=i+s});if(-1!==e)return c.charStart+e}if(n<=0)return-1;const i=function(e,t){let n=0,i=0;for(const o of e.runs)for(const e of o.glyphs)2!==e.flags&&(n+=e.height,i++);return 0===i?0:n/i*t}(e,n),o=i/2;let s=-1,r=1/0;for(const c of e.runs)if(!(t.y<c.rect.y-o||t.y>c.rect.y+c.rect.height+o||t.x<c.rect.x-o||t.x>c.rect.x+c.rect.width+o))for(let e=0;e<c.glyphs.length;e++){const n=c.glyphs[e];if(2===n.flags)continue;const i=n.tightX??n.x,a=n.tightY??n.y,l=n.tightWidth??n.width,d=n.tightHeight??n.height,h=i-o,u=i+l+o,g=a-o,p=a+d+o;if(t.x<h||t.x>u||t.y<g||t.y>p)continue;const m=Math.min(Math.abs(t.x-i),Math.abs(t.x-(i+l)))+Math.min(Math.abs(t.y-a),Math.abs(t.y-(a+d)));m<r&&(r=m,s=c.charStart+e)}return s}function w(e,t,n){if(!e||!t)return null;if(n<e.start.page||n>e.end.page)return null;const i=n===e.start.page?e.start.index:0,o=t.runs[t.runs.length-1],s=o.charStart+o.glyphs.length-1;return{from:i,to:n===e.end.page?e.end.index:s}}function $(e,t,n,i=!0){const o=[];for(const s of e.runs){const e=s.charStart,i=e+s.glyphs.length-1;if(i<t||e>n)continue;const r=Math.max(t,e)-e,c=Math.min(n,i)-e;let a=1/0,l=-1/0,d=1/0,h=-1/0,u=0,g=0,p=-1/0;const m=()=>{a!==1/0&&u>0&&o.push({rect:{origin:{x:a,y:d},size:{width:l-a,height:h-d}},charCount:u,fontSize:s.fontSize}),a=1/0,l=-1/0,d=1/0,h=-1/0,u=0,g=0,p=-1/0};for(let t=r;t<=c;t++){const e=s.glyphs[t];if(2!==e.flags){if(u>0&&p>-1/0){const t=Math.abs(e.x-p),n=g/u;n>0&&t>2.5*n&&m()}a=Math.min(a,e.x),l=Math.max(l,e.x+e.width),d=Math.min(d,e.y),h=Math.max(h,e.y+e.height),u++,g+=e.width,p=e.x+e.width}}m()}return i?k(o):o.map(e=>e.rect)}function D(e,t){const n=Math.min(e.origin.x,t.origin.x),i=Math.min(e.origin.y,t.origin.y);return{origin:{x:n,y:i},size:{width:Math.max(e.origin.x+e.size.width,t.origin.x+t.size.width)-n,height:Math.max(e.origin.y+e.size.height,t.origin.y+t.size.height)-i}}}function q(e,t){const n=Math.max(e.origin.x,t.origin.x),i=Math.max(e.origin.y,t.origin.y),o=Math.min(e.origin.x+e.size.width,t.origin.x+t.size.width),s=Math.min(e.origin.y+e.size.height,t.origin.y+t.size.height);return{origin:{x:n,y:i},size:{width:Math.max(0,o-n),height:Math.max(0,s-i)}}}function R(e){return e.size.width<=0||e.size.height<=0}function z(e,t){if(R(e)||R(t))return 0;const n=D(e,t);if(n.size.height===e.size.height||n.size.height===t.size.height)return 1;return q(e,t).size.height/n.size.height}function A(e,t){if(null!=e.fontSize&&null!=t.fontSize&&e.fontSize>0&&t.fontSize>0){if(Math.max(e.fontSize,t.fontSize)/Math.min(e.fontSize,t.fontSize)>1.5)return!1}const n=e.rect,i=t.rect;if(z(n,i)<.8)return!1;const o=1*n.size.width/e.charCount,s=1*i.size.width/t.charCount,r=n.origin.x-o,c=n.origin.x+n.size.width+o,a=i.origin.x-s;return r<i.origin.x+i.size.width+s&&c>a}function k(e){const t=[];let n=null,i=null;for(const o of e)n&&i?A(n,o)?i=D(i,o.rect):(t.push(i),i=o.rect):i=o.rect,n=o;return i&&!R(i)&&t.push(i),t}function N(e,t){for(let n=0;n<e.runs.length;n++){const i=e.runs[n],o=t-i.charStart;if(o>=0&&o<i.glyphs.length)return{runIdx:n,localIdx:o}}return null}function G(e){return 1===e||2===e}function O(e,t){if(!N(e,t))return null;const n=function(e){if(0===e.runs.length)return 0;const t=e.runs[e.runs.length-1];return t.charStart+t.glyphs.length}(e);if(0===n)return null;let i=t;for(;i>0;){const t=N(e,i-1);if(!t)break;if(G(e.runs[t.runIdx].glyphs[t.localIdx].flags))break;i--}let o=t;for(;o<n-1;){const t=N(e,o+1);if(!t)break;if(G(e.runs[t.runIdx].glyphs[t.localIdx].flags))break;o++}return{from:i,to:o}}function L(e,t){const n=N(e,t);if(!n)return null;const i=e.runs[n.runIdx],o=i.rect.y,s=i.rect.y+i.rect.height;let r=i.charStart,c=i.charStart+i.glyphs.length-1;for(let a=n.runIdx-1;a>=0;a--){const t=e.runs[a];if(!F(t)){if(!H(t.rect.y,t.rect.y+t.rect.height,o,s))break;r=t.charStart}}for(let a=n.runIdx+1;a<e.runs.length;a++){const t=e.runs[a];if(!F(t)){if(!H(t.rect.y,t.rect.y+t.rect.height,o,s))break;c=t.charStart+t.glyphs.length-1}}return{from:r,to:c}}function F(e){return 0===e.rect.width&&0===e.rect.height}function H(e,t,n,i){const o=Math.max(t,i)-Math.min(e,n),s=Math.max(0,Math.min(t,i)-Math.max(e,n));return 0!==o&&s/o>=.5}const B=class extends e.BasePlugin{constructor(n,i,o){var s,r,c;super(n,i),this.enabledModesPerDoc=new Map,this.selecting=new Map,this.anchor=new Map,this.hasTextAnchor=new Map,this.marqueePage=new Map,this.pageCallbacks=new Map,this.geoAccessOrder=new Map,this.menuPlacement$=e.createScopedEmitter((e,t)=>({documentId:e,placement:t})),this.selChange$=e.createScopedEmitter((e,t)=>({documentId:e,selection:t,modeId:this.interactionManagerCapability.forDocument(e).getActiveMode()})),this.textRetrieved$=e.createScopedEmitter((e,t)=>({documentId:e,text:t})),this.copyToClipboard$=e.createScopedEmitter((e,t)=>({documentId:e,text:t}),{cache:!1}),this.beginSelection$=e.createScopedEmitter((e,t)=>({documentId:e,page:t.page,index:t.index,modeId:t.modeId}),{cache:!1}),this.endSelection$=e.createScopedEmitter((e,t)=>({documentId:e,modeId:t.modeId}),{cache:!1}),this.marqueeChange$=e.createScopedEmitter((e,t)=>({documentId:e,pageIndex:t.pageIndex,rect:t.rect,modeId:t.modeId}),{cache:!1}),this.marqueeEnd$=e.createScopedEmitter((e,t)=>({documentId:e,pageIndex:t.pageIndex,rect:t.rect,modeId:t.modeId}),{cache:!1}),this.emptySpaceClick$=e.createScopedEmitter((e,t)=>({documentId:e,pageIndex:t.pageIndex,modeId:t.modeId}),{cache:!1}),this.viewportCapability=null,this.scrollCapability=null,this.config=o,this.menuHeight=o.menuHeight??40;const a=i.getPlugin("interaction-manager");if(!a)throw new Error("SelectionPlugin: InteractionManagerPlugin is required.");this.interactionManagerCapability=a.provides(),this.viewportCapability=(null==(s=i.getPlugin("viewport"))?void 0:s.provides())??null,this.scrollCapability=(null==(r=i.getPlugin("scroll"))?void 0:r.provides())??null,this.coreStore.onAction(e.REFRESH_PAGES,e=>{const{documentId:n,pageIndexes:i}=e.payload,o=i.map(e=>this.getNewPageGeometryAndCache(n,e));t.Task.all(o).wait(()=>{i.forEach(e=>{this.notifyPage(n,e)})},t.ignore)}),null==(c=this.viewportCapability)||c.onViewportChange(e=>{this.recalculateMenuPlacement(e.documentId)},{mode:"throttle",wait:100})}onDocumentLoadingStarted(e){var t;this.dispatch(((e,t)=>({type:o,payload:{documentId:e,state:t}}))(e,x));const n=!1!==(null==(t=this.config.marquee)?void 0:t.enabled);this.enabledModesPerDoc.set(e,new Map([["pointerMode",{enableSelection:!0,showSelectionRects:!0,enableMarquee:n,showMarqueeRects:!0}]])),this.pageCallbacks.set(e,new Map),this.geoAccessOrder.set(e,[]),this.selecting.set(e,!1),this.anchor.set(e,void 0),this.hasTextAnchor.set(e,!1)}onDocumentClosed(e){this.dispatch((e=>({type:s,payload:e}))(e)),this.enabledModesPerDoc.delete(e),this.pageCallbacks.delete(e),this.geoAccessOrder.delete(e),this.selecting.delete(e),this.hasTextAnchor.delete(e),this.anchor.delete(e),this.marqueePage.delete(e),this.selChange$.clearScope(e),this.textRetrieved$.clearScope(e),this.copyToClipboard$.clearScope(e),this.beginSelection$.clearScope(e),this.endSelection$.clearScope(e),this.menuPlacement$.clearScope(e),this.marqueeChange$.clearScope(e),this.marqueeEnd$.clearScope(e),this.emptySpaceClick$.clearScope(e)}async initialize(){}async destroy(){this.selChange$.clear(),this.textRetrieved$.clear(),this.copyToClipboard$.clear(),this.beginSelection$.clear(),this.endSelection$.clear(),this.menuPlacement$.clear(),this.marqueeChange$.clear(),this.marqueeEnd$.clear(),this.emptySpaceClick$.clear(),super.destroy()}buildCapability(){const e=e=>e??this.getActiveDocumentId();return{getFormattedSelection:t=>v(this.getDocumentState(e(t))),getFormattedSelectionForPage:(t,n)=>P(this.getDocumentState(e(n)),t),getHighlightRectsForPage:(t,n)=>M(this.getDocumentState(e(n)),t),getHighlightRects:t=>this.getDocumentState(e(t)).rects,getBoundingRectForPage:(t,n)=>I(this.getDocumentState(e(n)),t),getBoundingRects:t=>E(this.getDocumentState(e(t))),getSelectedText:t=>this.getSelectedText(e(t)),clear:t=>this.clearSelection(e(t)),copyToClipboard:t=>this.copyToClipboard(e(t)),getState:t=>this.getDocumentState(e(t)),enableForMode:(t,n,i)=>{var o;return null==(o=this.enabledModesPerDoc.get(e(i)))?void 0:o.set(t,{...n})},isEnabledForMode:(t,n)=>{var i;return(null==(i=this.enabledModesPerDoc.get(e(n)))?void 0:i.has(t))??!1},setMarqueeEnabled:(t,n)=>this.setMarqueeEnabled(e(n),t),isMarqueeEnabled:t=>this.isMarqueeEnabled(e(t)),forDocument:this.createSelectionScope.bind(this),onCopyToClipboard:this.copyToClipboard$.onGlobal,onSelectionChange:this.selChange$.onGlobal,onTextRetrieved:this.textRetrieved$.onGlobal,onBeginSelection:this.beginSelection$.onGlobal,onEndSelection:this.endSelection$.onGlobal,onMarqueeChange:this.marqueeChange$.onGlobal,onMarqueeEnd:this.marqueeEnd$.onGlobal,onEmptySpaceClick:this.emptySpaceClick$.onGlobal}}createSelectionScope(e){return{getFormattedSelection:()=>v(this.getDocumentState(e)),getFormattedSelectionForPage:t=>P(this.getDocumentState(e),t),getHighlightRectsForPage:t=>M(this.getDocumentState(e),t),getHighlightRects:()=>this.getDocumentState(e).rects,getBoundingRectForPage:t=>I(this.getDocumentState(e),t),getBoundingRects:()=>E(this.getDocumentState(e)),getSelectedText:()=>this.getSelectedText(e),clear:()=>this.clearSelection(e),copyToClipboard:()=>this.copyToClipboard(e),getState:()=>this.getDocumentState(e),setMarqueeEnabled:t=>this.setMarqueeEnabled(e,t),isMarqueeEnabled:()=>this.isMarqueeEnabled(e),onSelectionChange:this.selChange$.forScope(e),onTextRetrieved:this.textRetrieved$.forScope(e),onCopyToClipboard:this.copyToClipboard$.forScope(e),onBeginSelection:this.beginSelection$.forScope(e),onEndSelection:this.endSelection$.forScope(e),onMarqueeChange:this.marqueeChange$.forScope(e),onMarqueeEnd:this.marqueeEnd$.forScope(e),onEmptySpaceClick:this.emptySpaceClick$.forScope(e)}}getDocumentState(e){const t=this.state.documents[e];if(!t)throw new Error(`Selection state not found for document: ${e}`);return t}onMenuPlacement(e,t){return this.menuPlacement$.forScope(e)(t)}registerSelectionOnPage(e){var n;const{documentId:i,pageIndex:o,onRectsChange:s}=e,r=this.state.documents[i];if(!r)return this.logger.warn("SelectionPlugin","RegisterFailed",`Cannot register selection on page ${o} for document ${i}: document state not initialized.`),()=>{};null==(n=this.pageCallbacks.get(i))||n.set(o,s);const c=this.getOrLoadGeometry(i,o),a=this.interactionManagerCapability.forDocument(i),l=this.enabledModesPerDoc.get(i);s({rects:M(r,o),boundingRect:I(r,o)}),c.wait(e=>{const t=this.getDocumentState(i),n=t.selection;if(!n||o<n.start.page||o>n.end.page)return;const s=w(n,e,o);if(!s)return;const r=$(e,s.from,s.to);this.dispatch(S(i,{...t.rects,[o]:r})),this.dispatch(y(i,{...t.slices,[o]:{start:s.from,count:s.to-s.from+1}})),this.notifyPage(i,o)},t.ignore);const d=function(e){const t=e.minDragDistance??3,n=e.toleranceFactor??1.5;let i=null,o=null,s=!1,r=0;function c(){var t;i=null,o=null,s=!1,null==(t=e.setHasTextAnchor)||t.call(e,!1)}return{onPointerDown:(t,c,a)=>{var l,d;if(c.target===c.currentTarget&&(null==(l=e.onEmptySpaceClick)||l.call(e,a)),!e.isEnabled(a))return;const h=Date.now();(0===r||h-r>=500)&&e.onClear(a);const u=e.getGeometry();if(!u)return;const g=T(u,t,n);-1!==g&&(i=g,o=t,s=!1,null==(d=e.setHasTextAnchor)||d.call(e,!0))},onPointerMove:(r,c,a)=>{if(!e.isEnabled(a))return;const l=e.getGeometry();if(!l)return;const d=T(l,r,n);if(e.setCursor(-1!==d?"text":null),null!==i&&o&&!s){const n=r.x-o.x,c=r.y-o.y;return void(Math.sqrt(n*n+c*c)>=t&&(s=!0,e.onBegin(i,a),-1!==d&&e.onUpdate(d,a)))}e.isSelecting()&&-1!==d&&e.onUpdate(d,a)},onPointerUp:(t,n,i)=>{e.isEnabled(i)?(s&&e.onEnd(i),c()):c()},onDoubleClick:(t,i,o)=>{var s;if(!e.isEnabled(o))return;const c=e.getGeometry();if(!c)return;const a=T(c,t,n);-1!==a&&(r=Date.now(),null==(s=e.onWordSelect)||s.call(e,a,o))},onClick:(t,i,o)=>{var s;if(!e.isEnabled(o))return;if(0===r)return;if(Date.now()-r>500)return void(r=0);r=0;const c=e.getGeometry();if(!c)return;const a=T(c,t,n);-1!==a&&(null==(s=e.onLineSelect)||s.call(e,a,o))},onHandlerActiveEnd:t=>{c(),e.isEnabled(t)&&e.onClear(t)}}}({getGeometry:()=>this.getDocumentState(i).geometry[o],isEnabled:e=>{const t=null==l?void 0:l.get(e);return!!t&&!1!==t.enableSelection},onBegin:(e,t)=>this.beginSelection(i,o,e,t),onUpdate:(e,t)=>this.updateSelection(i,o,e,t),onEnd:e=>this.endSelection(i,e),onClear:e=>this.clearSelection(i,e),isSelecting:()=>this.selecting.get(i)??!1,setCursor:e=>e?a.setCursor("selection-text",e,10):a.removeCursor("selection-text"),onEmptySpaceClick:e=>this.emptySpaceClick$.emit(i,{pageIndex:o,modeId:e}),onWordSelect:(e,t)=>this.selectWord(i,o,e,t),onLineSelect:(e,t)=>this.selectLine(i,o,e,t),setHasTextAnchor:e=>this.hasTextAnchor.set(i,e),minDragDistance:this.config.minSelectionDragDistance,toleranceFactor:this.config.toleranceFactor}),h=this.interactionManagerCapability.registerAlways({scope:{type:"page",documentId:i,pageIndex:o},handlers:d});return()=>{var e;h(),null==(e=this.pageCallbacks.get(i))||e.delete(o),c.abort({code:t.PdfErrorCode.Cancelled,message:"Cleanup"})}}registerMarqueeOnPage(t){var n;const{documentId:i,pageIndex:o,scale:s,onRectChange:r}=t;if(!this.state.documents[i])return this.logger.warn("SelectionPlugin","RegisterMarqueeFailed",`Cannot register marquee on page ${o} for document ${i}: document state not initialized.`),()=>{};const c=this.coreState.core.documents[i];if(!c||!c.document)return this.logger.warn("SelectionPlugin","DocumentNotFound",`Cannot register marquee on page ${o}: document not found`),()=>{};const a=c.document.pages[o];if(!a)return this.logger.warn("SelectionPlugin","PageNotFound",`Cannot register marquee on page ${o}: page not found`),()=>{};const l=a.size,d=(null==(n=this.config.marquee)?void 0:n.minDragPx)??5,h=()=>{var e;const t=this.interactionManagerCapability.forDocument(i).getActiveMode(),n=null==(e=this.enabledModesPerDoc.get(i))?void 0:e.get(t);return!1!==(null==n?void 0:n.showMarqueeRects)},u=function(t){const{pageSize:n,scale:i,minDragPx:o=5}=t;let s=null,r=null;return{onPointerDown:(e,n,i)=>{var o,c;t.isEnabled(i)&&((null==(o=t.isTextSelecting)?void 0:o.call(t))||(s=e,r={origin:{x:e.x,y:e.y},size:{width:0,height:0}},t.onBegin(e,i),null==(c=n.setPointerCapture)||c.call(n)))},onPointerMove:(i,o,c)=>{if(!s||!t.isEnabled(c))return;const a=e.clamp(i.x,0,n.width),l=e.clamp(i.y,0,n.height);r={origin:{x:Math.min(s.x,a),y:Math.min(s.y,l)},size:{width:Math.abs(a-s.x),height:Math.abs(l-s.y)}},t.onChange(r,c)},onPointerUp:(e,n,c)=>{var a;t.isEnabled(c)&&(r&&s&&(Math.max(r.size.width,r.size.height)*i>o?t.onEnd(r,c):t.onCancel(c)),s=null,r=null,null==(a=n.releasePointerCapture)||a.call(n))},onPointerCancel:(e,n,i)=>{var o;t.isEnabled(i)&&(s=null,r=null,t.onCancel(i),null==(o=n.releasePointerCapture)||o.call(n))}}}({pageSize:l,scale:s,minDragPx:d,isEnabled:e=>{var t;const n=null==(t=this.enabledModesPerDoc.get(i))?void 0:t.get(e);return!0===(null==n?void 0:n.enableMarquee)},isTextSelecting:()=>(this.selecting.get(i)??!1)||(this.hasTextAnchor.get(i)??!1),onBegin:(e,t)=>this.beginMarquee(i,o,e,t),onChange:(e,t)=>{this.updateMarquee(i,o,e,t),r(h()?e:null)},onEnd:(e,t)=>{this.endMarquee(i,o,e,t),r(null)},onCancel:e=>{this.cancelMarquee(i,e),r(null)}});return this.interactionManagerCapability.registerAlways({scope:{type:"page",documentId:i,pageIndex:o},handlers:u})}getPlacementMetrics(e,t,n,i){var o;const s=null==(o=this.scrollCapability)?void 0:o.forDocument(e),r=null==s?void 0:s.getRectPositionForPage(t,n);if(!r)return null;const c=r.origin.y-i.scrollTop,a=r.origin.y+r.size.height-i.scrollTop;return{pageIndex:t,rect:n,spaceAbove:c,spaceBelow:i.clientHeight-a,isBottomVisible:a>0&&a<=i.clientHeight,isTopVisible:c>=0&&c<i.clientHeight}}emitMenuPlacement(e,t){this.menuPlacement$.emit(e,t),t?this.interactionManagerCapability.claimPageActivity(e,"selection-menu",t.pageIndex):this.interactionManagerCapability.releasePageActivity(e,"selection-menu")}recalculateMenuPlacement(e){const t=this.state.documents[e];if(!t)return;if(t.selecting||null===t.selection)return void this.emitMenuPlacement(e,null);const n=E(t);if(0===n.length)return void this.emitMenuPlacement(e,null);const i=n[n.length-1];if(!this.viewportCapability||!this.scrollCapability)return void this.emitMenuPlacement(e,{pageIndex:i.page,rect:i.rect,spaceAbove:0,spaceBelow:1/0,suggestTop:!1,isVisible:!0});const o=this.viewportCapability.forDocument(e).getMetrics(),s=n[0],r=this.getPlacementMetrics(e,i.page,i.rect,o),c=this.getPlacementMetrics(e,s.page,s.rect,o);r&&r.isBottomVisible&&r.spaceBelow>this.menuHeight?this.emitMenuPlacement(e,{...r,suggestTop:!1,isVisible:!0}):c&&c.isTopVisible?this.emitMenuPlacement(e,{...c,suggestTop:!0,isVisible:!0}):r&&r.isBottomVisible?this.emitMenuPlacement(e,{...r,suggestTop:!1,isVisible:!0}):this.emitMenuPlacement(e,null)}notifyPage(e,t){var n,i;const o=null==(n=this.pageCallbacks.get(e))?void 0:n.get(t);if(o){const n=this.getDocumentState(e),s=this.interactionManagerCapability.forDocument(e).getActiveMode(),r=null==(i=this.enabledModesPerDoc.get(e))?void 0:i.get(s);o(r&&!1!==(r.showSelectionRects??r.showRects)?{rects:M(n,t),boundingRect:I(n,t)}:{rects:[],boundingRect:null})}}notifyAllPages(e){var t;null==(t=this.pageCallbacks.get(e))||t.forEach((t,n)=>{this.notifyPage(e,n)})}getNewPageGeometryAndCache(e,n){const i=this.getCoreDocument(e);if(!i||!i.document)return t.PdfTaskHelper.reject({code:t.PdfErrorCode.NotFound,message:"Doc Not Found"});const o=i.document.pages.find(e=>e.index===n),s=this.engine.getPageGeometry(i.document,o);return s.wait(t=>{this.dispatch(((e,t,n)=>({type:r,payload:{documentId:e,page:t,geo:n}}))(e,n,t)),this.touchGeometry(e,n)},t.ignore),s}getOrLoadGeometry(e,n){const i=this.getDocumentState(e).geometry[n];return i?(this.touchGeometry(e,n),t.PdfTaskHelper.resolve(i)):this.getNewPageGeometryAndCache(e,n)}touchGeometry(e,t){const n=this.geoAccessOrder.get(e);if(!n)return;const i=n.indexOf(t);i>-1&&n.splice(i,1),n.push(t),this.evictGeometryIfNeeded(e)}evictGeometryIfNeeded(e){const t=this.config.maxCachedGeometries??50,n=this.geoAccessOrder.get(e);if(!n||n.length<=t)return;const i=this.pageCallbacks.get(e),o=[];for(;n.length-o.length>t;){const e=n.find(e=>!o.includes(e)&&!(null==i?void 0:i.has(e)));if(void 0===e)break;o.push(e)}if(0!==o.length){for(const e of o){const t=n.indexOf(e);t>-1&&n.splice(t,1)}this.dispatch(((e,t)=>({type:g,payload:{documentId:e,pages:t}}))(e,o))}}beginSelection(e,t,n,i){this.selecting.set(e,!0),this.anchor.set(e,{page:t,index:n}),this.dispatch(m(e)),this.beginSelection$.emit(e,{page:t,index:n,modeId:i}),this.recalculateMenuPlacement(e)}endSelection(e,t){this.selecting.set(e,!1),this.anchor.set(e,void 0),this.dispatch(f(e)),this.endSelection$.emit(e,{modeId:t}),this.recalculateMenuPlacement(e)}clearSelection(e,t){this.selecting.set(e,!1),this.anchor.set(e,void 0),this.dispatch((e=>({type:d,payload:{documentId:e}}))(e)),this.selChange$.emit(e,null),this.emitMenuPlacement(e,null),this.notifyAllPages(e)}selectWord(e,t,n,i){const o=this.getDocumentState(e).geometry[t];if(!o)return;const s=O(o,n);s&&this.applyInstantSelection(e,t,s.from,s.to,i)}selectLine(e,t,n,i){const o=this.getDocumentState(e).geometry[t];if(!o)return;const s=L(o,n);s&&this.applyInstantSelection(e,t,s.from,s.to,i)}applyInstantSelection(e,t,n,i,o){const s={start:{page:t,index:n},end:{page:t,index:i}};this.selecting.set(e,!1),this.anchor.set(e,void 0),this.dispatch(m(e)),this.dispatch(p(e,s)),this.updateRectsAndSlices(e,s),this.dispatch(f(e)),this.selChange$.emit(e,s),this.beginSelection$.emit(e,{page:t,index:n,modeId:o}),this.endSelection$.emit(e,{modeId:o});for(let r=s.start.page;r<=s.end.page;r++)this.notifyPage(e,r);this.recalculateMenuPlacement(e)}updateSelection(e,t,n,i){if(!this.selecting.get(e)||!this.anchor.get(e))return;const o=this.anchor.get(e),s=t>o.page||t===o.page&&n>=o.index,r={start:s?o:{page:t,index:n},end:s?{page:t,index:n}:o};this.dispatch(p(e,r)),this.updateRectsAndSlices(e,r),this.selChange$.emit(e,r);for(let c=r.start.page;c<=r.end.page;c++)this.notifyPage(e,c)}updateRectsAndSlices(e,t){const n=this.getDocumentState(e),i={},o={};for(let s=t.start.page;s<=t.end.page;s++){const e=n.geometry[s],r=w(t,e,s);r&&(i[s]=$(e,r.from,r.to),o[s]={start:r.from,count:r.to-r.from+1})}this.dispatch(S(e,i)),this.dispatch(y(e,o))}getSelectedText(e){if(!this.checkPermission(e,t.PdfPermissionFlag.CopyContents))return this.logger.debug("SelectionPlugin","GetSelectedText",`Cannot get selected text: document ${e} lacks CopyContents permission`),t.PdfTaskHelper.reject({code:t.PdfErrorCode.Security,message:"Document lacks CopyContents permission"});const n=this.getCoreDocument(e),i=this.getDocumentState(e);if(!(null==n?void 0:n.document)||!i.selection)return t.PdfTaskHelper.reject({code:t.PdfErrorCode.NotFound,message:"Doc Not Found or No Selection"});const o=i.selection,s=[];for(let t=o.start.page;t<=o.end.page;t++){const e=i.slices[t];e&&s.push({pageIndex:t,charIndex:e.start,charCount:e.count})}if(0===s.length)return t.PdfTaskHelper.resolve([]);const r=this.engine.getTextSlices(n.document,s);return r.wait(t=>{this.textRetrieved$.emit(e,t)},t.ignore),r}copyToClipboard(e){if(!this.checkPermission(e,t.PdfPermissionFlag.CopyContents))return void this.logger.debug("SelectionPlugin","CopyToClipboard",`Cannot copy to clipboard: document ${e} lacks CopyContents permission`);this.getSelectedText(e).wait(t=>{this.copyToClipboard$.emit(e,t.join("\n"))},t.ignore)}beginMarquee(e,t,n,i){this.marqueePage.set(e,t)}updateMarquee(e,t,n,i){this.marqueeChange$.emit(e,{pageIndex:t,rect:n,modeId:i})}endMarquee(e,t,n,i){this.marqueeEnd$.emit(e,{pageIndex:t,rect:n,modeId:i}),this.marqueeChange$.emit(e,{pageIndex:t,rect:null,modeId:i}),this.marqueePage.delete(e)}cancelMarquee(e,t){const n=this.marqueePage.get(e);void 0!==n&&(this.marqueeChange$.emit(e,{pageIndex:n,rect:null,modeId:t}),this.marqueePage.delete(e))}setMarqueeEnabled(e,t){const n=this.enabledModesPerDoc.get(e);if(!n)return;const i=n.get("pointerMode");i?i.enableMarquee=t:t&&n.set("pointerMode",{enableMarquee:!0})}isMarqueeEnabled(e){var t;const n=null==(t=this.enabledModesPerDoc.get(e))?void 0:t.get("pointerMode");return!1!==(null==n?void 0:n.enableMarquee)}};B.id="selection";let _=B;const V={manifest:i,create:(e,t)=>new _(n,e,t),reducer:(e=C,t)=>{switch(t.type){case o:{const{documentId:n,state:i}=t.payload;return b(e,n,i)}case s:{const n=t.payload,{[n]:i,...o}=e.documents;return{...e,documents:o}}case r:{const{documentId:n,page:i,geo:o}=t.payload,s=e.documents[n];return s?b(e,n,{...s,geometry:{...s.geometry,[i]:o}}):e}case c:{const{documentId:n,selection:i}=t.payload,o=e.documents[n];return o?b(e,n,{...o,selection:i,active:!0}):e}case a:{const{documentId:n}=t.payload,i=e.documents[n];return i?b(e,n,{...i,selecting:!0,selection:null,rects:{}}):e}case l:{const{documentId:n}=t.payload,i=e.documents[n];return i?b(e,n,{...i,selecting:!1}):e}case d:{const{documentId:n}=t.payload,i=e.documents[n];return i?b(e,n,{...i,selecting:!1,selection:null,rects:{},active:!1}):e}case h:{const{documentId:n,rects:i}=t.payload,o=e.documents[n];return o?b(e,n,{...o,rects:i}):e}case u:{const{documentId:n,slices:i}=t.payload,o=e.documents[n];return o?b(e,n,{...o,slices:i}):e}case g:{const{documentId:n,pages:i}=t.payload,o=e.documents[n];if(!o)return e;const s={...o.geometry},r={...o.rects},c={...o.slices};for(const e of i)delete s[e],delete r[e],delete c[e];return b(e,n,{...o,geometry:s,rects:r,slices:c})}case"SELECTION/RESET":{const{documentId:n}=t.payload;return e.documents[n]?b(e,n,x):e}default:return e}},initialState:C};exports.SELECTION_PLUGIN_ID=n,exports.SelectionPlugin=_,exports.SelectionPluginPackage=V,exports.expandToLineBoundary=L,exports.expandToWordBoundary=O,exports.getVerticalOverlap=z,exports.glyphAt=T,exports.manifest=i,exports.mergeAdjacentRects=k,exports.rectIntersect=q,exports.rectIsEmpty=R,exports.rectUnion=D,exports.rectsWithinSlice=$,exports.shouldMergeHorizontalRects=A,exports.sliceBounds=w;
|
|
2
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _embedpdf_core = require("@embedpdf/core");
|
|
3
|
+
let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
|
|
4
|
+
let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
|
|
5
|
+
//#region src/geometry.ts
|
|
6
|
+
/**
|
|
7
|
+
* The pure selection geometry: engine text geometry (PDF user space, y-up) →
|
|
8
|
+
* viewer content space (y-down), glyph hit-testing, word/line expansion, and the
|
|
9
|
+
* line-merge. No DOM, no engine, no React.
|
|
10
|
+
*
|
|
11
|
+
* The non-trivial bits are ports of battle-tested algorithms, adapted to v3's
|
|
12
|
+
* content-space `Rect {x,y,width,height}`:
|
|
13
|
+
* - `glyphAt` — PDFium `GetIndexAtPos` (exact tight box, then a tolerance pass).
|
|
14
|
+
* - `rectsForRange` — Chromium `pdfium_range.cc` `MergeAdjacentRects` (one rect per line).
|
|
15
|
+
* - `expandToWord/Line` — Chromium `OnMultipleClick` (double = word, triple = line).
|
|
16
|
+
* The PDF↔content y-flip is the geometry package's `pageGeometry` (the one place
|
|
17
|
+
* that math lives), never hand-rolled here.
|
|
18
|
+
*/
|
|
19
|
+
const FLAG_EMPTY = 2;
|
|
20
|
+
const isBoundary = (flags) => (flags & 3) !== 0;
|
|
21
|
+
const isEmpty = (flags) => (flags & FLAG_EMPTY) !== 0;
|
|
22
|
+
const toContent = (m, b) => (0, _embedpdf_core_geometry.applyRect)(m, {
|
|
23
|
+
x: b.left,
|
|
24
|
+
y: b.bottom,
|
|
25
|
+
width: b.right - b.left,
|
|
26
|
+
height: b.top - b.bottom
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Flatten a page's text geometry into content-space glyphs + runs. Rotation does
|
|
30
|
+
* NOT enter here — the overlay rides the page's CSS rotation, so glyphs stay in
|
|
31
|
+
* un-rotated content space (the crop-relative y-flip is the only conversion).
|
|
32
|
+
*/
|
|
33
|
+
function buildPageText(snapshot, crop, rotation, userUnit) {
|
|
34
|
+
const { pdfToContent } = (0, _embedpdf_core_geometry.pageGeometry)({
|
|
35
|
+
crop,
|
|
36
|
+
rotation,
|
|
37
|
+
userUnit
|
|
38
|
+
}, 1);
|
|
39
|
+
const glyphs = [];
|
|
40
|
+
const runs = [];
|
|
41
|
+
for (const run of snapshot.runs) {
|
|
42
|
+
const start = glyphs.length;
|
|
43
|
+
for (const g of run.glyphs) glyphs.push({
|
|
44
|
+
loose: toContent(pdfToContent, g.looseBox),
|
|
45
|
+
tight: g.tightBox ? toContent(pdfToContent, g.tightBox) : void 0,
|
|
46
|
+
flags: g.flags
|
|
47
|
+
});
|
|
48
|
+
runs.push({
|
|
49
|
+
start,
|
|
50
|
+
count: run.glyphs.length,
|
|
51
|
+
rect: toContent(pdfToContent, run.rect),
|
|
52
|
+
fontSize: run.fontSize
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
glyphs,
|
|
57
|
+
runs
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function avgGlyphHeight(text) {
|
|
61
|
+
let total = 0;
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const g of text.glyphs) {
|
|
64
|
+
if (isEmpty(g.flags)) continue;
|
|
65
|
+
total += g.loose.height;
|
|
66
|
+
count++;
|
|
67
|
+
}
|
|
68
|
+
return count === 0 ? 0 : total / count;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The glyph index at a content-space point, or null when nothing is near (so the
|
|
72
|
+
* caller can show the pointer cursor off-text). PDFium `GetIndexAtPos`: exact
|
|
73
|
+
* tight-box containment first, then a tolerance pass (closest by Manhattan
|
|
74
|
+
* distance within `toleranceFactor × average glyph height`).
|
|
75
|
+
*/
|
|
76
|
+
function glyphAt(text, p, toleranceFactor = 1.5) {
|
|
77
|
+
for (const run of text.runs) {
|
|
78
|
+
const r = run.rect;
|
|
79
|
+
if (p.x < r.x || p.x > r.x + r.width || p.y < r.y || p.y > r.y + r.height) continue;
|
|
80
|
+
for (let i = 0; i < run.count; i++) {
|
|
81
|
+
const b = text.glyphs[run.start + i].tight ?? text.glyphs[run.start + i].loose;
|
|
82
|
+
if (p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height) return run.start + i;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (toleranceFactor <= 0) return null;
|
|
86
|
+
const half = avgGlyphHeight(text) * toleranceFactor / 2;
|
|
87
|
+
let best = -1;
|
|
88
|
+
let bestDist = Infinity;
|
|
89
|
+
for (const run of text.runs) {
|
|
90
|
+
const r = run.rect;
|
|
91
|
+
if (p.y < r.y - half || p.y > r.y + r.height + half || p.x < r.x - half || p.x > r.x + r.width + half) continue;
|
|
92
|
+
for (let i = 0; i < run.count; i++) {
|
|
93
|
+
const g = text.glyphs[run.start + i];
|
|
94
|
+
if (isEmpty(g.flags)) continue;
|
|
95
|
+
const b = g.tight ?? g.loose;
|
|
96
|
+
if (p.x < b.x - half || p.x > b.x + b.width + half || p.y < b.y - half || p.y > b.y + b.height + half) continue;
|
|
97
|
+
const dx = Math.min(Math.abs(p.x - b.x), Math.abs(p.x - (b.x + b.width)));
|
|
98
|
+
const dy = Math.min(Math.abs(p.y - b.y), Math.abs(p.y - (b.y + b.height)));
|
|
99
|
+
if (dx + dy < bestDist) {
|
|
100
|
+
bestDist = dx + dy;
|
|
101
|
+
best = run.start + i;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return best >= 0 ? best : null;
|
|
106
|
+
}
|
|
107
|
+
/** Double-click: the word around `i` (walk to space/empty glyphs both ways). */
|
|
108
|
+
function expandToWord(text, i) {
|
|
109
|
+
const n = text.glyphs.length;
|
|
110
|
+
if (i < 0 || i >= n) return [i, i];
|
|
111
|
+
let from = i;
|
|
112
|
+
while (from > 0 && !isBoundary(text.glyphs[from - 1].flags)) from--;
|
|
113
|
+
let to = i;
|
|
114
|
+
while (to < n - 1 && !isBoundary(text.glyphs[to + 1].flags)) to++;
|
|
115
|
+
return [from, to];
|
|
116
|
+
}
|
|
117
|
+
/** Triple-click: the full visual line — runs whose vertical extent overlaps the anchor run's. */
|
|
118
|
+
function expandToLine(text, i) {
|
|
119
|
+
const ri = text.runs.findIndex((r) => i >= r.start && i < r.start + r.count);
|
|
120
|
+
if (ri < 0) return [i, i];
|
|
121
|
+
const anchor = text.runs[ri];
|
|
122
|
+
const top = anchor.rect.y;
|
|
123
|
+
const bottom = anchor.rect.y + anchor.rect.height;
|
|
124
|
+
let from = anchor.start;
|
|
125
|
+
let to = anchor.start + anchor.count - 1;
|
|
126
|
+
for (let r = ri - 1; r >= 0; r--) {
|
|
127
|
+
const run = text.runs[r];
|
|
128
|
+
if (isZero(run.rect)) continue;
|
|
129
|
+
if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;
|
|
130
|
+
from = run.start;
|
|
131
|
+
}
|
|
132
|
+
for (let r = ri + 1; r < text.runs.length; r++) {
|
|
133
|
+
const run = text.runs[r];
|
|
134
|
+
if (isZero(run.rect)) continue;
|
|
135
|
+
if (!overlapV(run.rect.y, run.rect.y + run.rect.height, top, bottom)) break;
|
|
136
|
+
to = run.start + run.count - 1;
|
|
137
|
+
}
|
|
138
|
+
return [from, to];
|
|
139
|
+
}
|
|
140
|
+
const CHAR_DISTANCE_FACTOR = 2.5;
|
|
141
|
+
const FONT_SIZE_RATIO_THRESHOLD = 1.5;
|
|
142
|
+
const VERTICAL_OVERLAP_THRESHOLD = .8;
|
|
143
|
+
const LINE_OVERLAP_THRESHOLD = .5;
|
|
144
|
+
function rectsForRange(text, from, to) {
|
|
145
|
+
const lo = Math.max(0, Math.min(from, to));
|
|
146
|
+
const hi = Math.min(text.glyphs.length - 1, Math.max(from, to));
|
|
147
|
+
const subRuns = [];
|
|
148
|
+
for (const run of text.runs) {
|
|
149
|
+
const runEnd = run.start + run.count - 1;
|
|
150
|
+
if (runEnd < lo || run.start > hi) continue;
|
|
151
|
+
const s = Math.max(lo, run.start);
|
|
152
|
+
const e = Math.min(hi, runEnd);
|
|
153
|
+
let minX = Infinity;
|
|
154
|
+
let maxX = -Infinity;
|
|
155
|
+
let minY = Infinity;
|
|
156
|
+
let maxY = -Infinity;
|
|
157
|
+
let charCount = 0;
|
|
158
|
+
let widthSum = 0;
|
|
159
|
+
let prevRight = -Infinity;
|
|
160
|
+
const flush = () => {
|
|
161
|
+
if (minX !== Infinity && charCount > 0) subRuns.push({
|
|
162
|
+
rect: {
|
|
163
|
+
x: minX,
|
|
164
|
+
y: minY,
|
|
165
|
+
width: maxX - minX,
|
|
166
|
+
height: maxY - minY
|
|
167
|
+
},
|
|
168
|
+
charCount,
|
|
169
|
+
fontSize: run.fontSize
|
|
170
|
+
});
|
|
171
|
+
minX = Infinity;
|
|
172
|
+
maxX = -Infinity;
|
|
173
|
+
minY = Infinity;
|
|
174
|
+
maxY = -Infinity;
|
|
175
|
+
charCount = 0;
|
|
176
|
+
widthSum = 0;
|
|
177
|
+
prevRight = -Infinity;
|
|
178
|
+
};
|
|
179
|
+
for (let gi = s; gi <= e; gi++) {
|
|
180
|
+
const g = text.glyphs[gi];
|
|
181
|
+
if (isEmpty(g.flags)) continue;
|
|
182
|
+
const b = g.loose;
|
|
183
|
+
if (charCount > 0 && prevRight > -Infinity) {
|
|
184
|
+
const avg = widthSum / charCount;
|
|
185
|
+
if (avg > 0 && Math.abs(b.x - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
|
|
186
|
+
}
|
|
187
|
+
minX = Math.min(minX, b.x);
|
|
188
|
+
maxX = Math.max(maxX, b.x + b.width);
|
|
189
|
+
minY = Math.min(minY, b.y);
|
|
190
|
+
maxY = Math.max(maxY, b.y + b.height);
|
|
191
|
+
charCount++;
|
|
192
|
+
widthSum += b.width;
|
|
193
|
+
prevRight = b.x + b.width;
|
|
194
|
+
}
|
|
195
|
+
flush();
|
|
196
|
+
}
|
|
197
|
+
return mergeAdjacentRects(subRuns);
|
|
198
|
+
}
|
|
199
|
+
function mergeAdjacentRects(runs) {
|
|
200
|
+
const out = [];
|
|
201
|
+
let prev = null;
|
|
202
|
+
let cur = null;
|
|
203
|
+
for (const run of runs) {
|
|
204
|
+
if (prev && cur && shouldMerge(prev, run)) cur = (0, _embedpdf_core_geometry.boundsOfRects)([cur, run.rect]);
|
|
205
|
+
else {
|
|
206
|
+
if (cur) out.push(cur);
|
|
207
|
+
cur = run.rect;
|
|
208
|
+
}
|
|
209
|
+
prev = run;
|
|
210
|
+
}
|
|
211
|
+
if (cur && cur.width > 0 && cur.height > 0) out.push(cur);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function shouldMerge(a, b) {
|
|
215
|
+
if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
|
|
216
|
+
if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
|
|
217
|
+
}
|
|
218
|
+
if (verticalOverlap(a.rect, b.rect) < VERTICAL_OVERLAP_THRESHOLD) return false;
|
|
219
|
+
const aw = a.rect.width / a.charCount;
|
|
220
|
+
const bw = b.rect.width / b.charCount;
|
|
221
|
+
const aL = a.rect.x - aw;
|
|
222
|
+
const aR = a.rect.x + a.rect.width + aw;
|
|
223
|
+
const bL = b.rect.x - bw;
|
|
224
|
+
return aL < b.rect.x + b.rect.width + bw && aR > bL;
|
|
225
|
+
}
|
|
226
|
+
function verticalOverlap(a, b) {
|
|
227
|
+
if (a.height <= 0 || b.height <= 0) return 0;
|
|
228
|
+
const u = Math.max(a.y + a.height, b.y + b.height) - Math.min(a.y, b.y);
|
|
229
|
+
if (u === a.height || u === b.height) return 1;
|
|
230
|
+
return Math.max(0, Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y)) / u;
|
|
231
|
+
}
|
|
232
|
+
const isZero = (r) => r.width === 0 && r.height === 0;
|
|
233
|
+
function overlapV(top1, bottom1, top2, bottom2) {
|
|
234
|
+
const u = Math.max(bottom1, bottom2) - Math.min(top1, top2);
|
|
235
|
+
if (u === 0) return false;
|
|
236
|
+
return Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2)) / u >= LINE_OVERLAP_THRESHOLD;
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/capability.ts
|
|
240
|
+
const EMPTY = [];
|
|
241
|
+
/**
|
|
242
|
+
* The selection capability. The reducer state holds the selection range, the
|
|
243
|
+
* derived content-space rects (per page), and per-page loaded flags; the (large,
|
|
244
|
+
* non-serializable) per-page text geometry is cached HERE in the closure.
|
|
245
|
+
*
|
|
246
|
+
* Selection is cross-page: glyphs are ordered globally by (pageIndex, glyph), so
|
|
247
|
+
* a drag from page 2 into page 4 selects the tail of 2, all of 3, and the head of
|
|
248
|
+
* 4. `recompute` rebuilds the merged line rects for every loaded page in the span
|
|
249
|
+
* and re-runs whenever a mid-span page finishes loading.
|
|
250
|
+
*/
|
|
251
|
+
function createSelectionCapability(ctx) {
|
|
252
|
+
const cache = /* @__PURE__ */ new Map();
|
|
253
|
+
const pending = /* @__PURE__ */ new Set();
|
|
254
|
+
const changeCbs = /* @__PURE__ */ new Set();
|
|
255
|
+
const commitCbs = /* @__PURE__ */ new Set();
|
|
256
|
+
const fireChange = () => changeCbs.forEach((cb) => cb());
|
|
257
|
+
const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((p) => p.pageObjectNumber === pon) ?? -1;
|
|
258
|
+
const ponAtIndex = (i) => ctx.document()?.pages[i]?.pageObjectNumber;
|
|
259
|
+
function orderedEnds(sel) {
|
|
260
|
+
const ai = pageIndexOf(sel.anchor.pon);
|
|
261
|
+
const fi = pageIndexOf(sel.focus.pon);
|
|
262
|
+
return ai < fi || ai === fi && sel.anchor.glyph <= sel.focus.glyph ? {
|
|
263
|
+
start: sel.anchor,
|
|
264
|
+
end: sel.focus,
|
|
265
|
+
direction: "forward"
|
|
266
|
+
} : {
|
|
267
|
+
start: sel.focus,
|
|
268
|
+
end: sel.anchor,
|
|
269
|
+
direction: "backward"
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function endpointFor(ptr, which) {
|
|
273
|
+
const rects = ctx.getState().rects[ptr.pon] ?? EMPTY;
|
|
274
|
+
if (!rects.length) return null;
|
|
275
|
+
return {
|
|
276
|
+
pon: ptr.pon,
|
|
277
|
+
rect: which === "start" ? rects[0] : rects[rects.length - 1]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function snapshot() {
|
|
281
|
+
const { selection, rects } = ctx.getState();
|
|
282
|
+
const pages = Object.keys(rects).map(Number).filter((pon) => (rects[pon]?.length ?? 0) > 0).map((pon) => ({
|
|
283
|
+
pon,
|
|
284
|
+
rects: rects[pon]
|
|
285
|
+
}));
|
|
286
|
+
if (!selection) return {
|
|
287
|
+
pages,
|
|
288
|
+
start: null,
|
|
289
|
+
end: null,
|
|
290
|
+
direction: "forward"
|
|
291
|
+
};
|
|
292
|
+
const { start, end, direction } = orderedEnds(selection);
|
|
293
|
+
return {
|
|
294
|
+
pages,
|
|
295
|
+
start: endpointFor(start, "start"),
|
|
296
|
+
end: endpointFor(end, "end"),
|
|
297
|
+
direction
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function ensurePage(pon) {
|
|
301
|
+
if (cache.has(pon) || pending.has(pon)) return;
|
|
302
|
+
const doc = ctx.doc;
|
|
303
|
+
const layout = ctx.document()?.pages.find((p) => p.pageObjectNumber === pon);
|
|
304
|
+
if (!doc || !layout) return;
|
|
305
|
+
pending.add(pon);
|
|
306
|
+
doc.page(pon).geometry.read().then((snapshot) => {
|
|
307
|
+
pending.delete(pon);
|
|
308
|
+
cache.set(pon, buildPageText(snapshot, layout.boxes.crop, layout.rotation, layout.userUnit));
|
|
309
|
+
ctx.dispatch({
|
|
310
|
+
type: "PAGE_LOADED",
|
|
311
|
+
pon
|
|
312
|
+
});
|
|
313
|
+
if (ctx.getState().selection) recompute();
|
|
314
|
+
}, () => {
|
|
315
|
+
pending.delete(pon);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function recompute(sel = ctx.getState().selection) {
|
|
319
|
+
if (!sel) return;
|
|
320
|
+
const { start, end } = orderedEnds(sel);
|
|
321
|
+
const si = pageIndexOf(start.pon);
|
|
322
|
+
const ei = pageIndexOf(end.pon);
|
|
323
|
+
if (si < 0 || ei < 0) return;
|
|
324
|
+
const rects = {};
|
|
325
|
+
for (let i = si; i <= ei; i++) {
|
|
326
|
+
const pon = ponAtIndex(i);
|
|
327
|
+
if (pon == null) continue;
|
|
328
|
+
const text = cache.get(pon);
|
|
329
|
+
if (!text) {
|
|
330
|
+
ensurePage(pon);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
rects[pon] = rectsForRange(text, i === si ? start.glyph : 0, i === ei ? end.glyph : text.glyphs.length - 1);
|
|
334
|
+
}
|
|
335
|
+
ctx.dispatch({
|
|
336
|
+
type: "SET",
|
|
337
|
+
selection: sel,
|
|
338
|
+
rects
|
|
339
|
+
});
|
|
340
|
+
fireChange();
|
|
341
|
+
}
|
|
342
|
+
function selectSpan(pon, point, expand) {
|
|
343
|
+
const text = cache.get(pon);
|
|
344
|
+
if (!text) return;
|
|
345
|
+
const i = glyphAt(text, point);
|
|
346
|
+
if (i == null) return;
|
|
347
|
+
const [from, to] = expand === "word" ? expandToWord(text, i) : expandToLine(text, i);
|
|
348
|
+
recompute({
|
|
349
|
+
anchor: {
|
|
350
|
+
pon,
|
|
351
|
+
glyph: from
|
|
352
|
+
},
|
|
353
|
+
focus: {
|
|
354
|
+
pon,
|
|
355
|
+
glyph: to
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
ensurePage,
|
|
361
|
+
isLoaded: (pon) => !!ctx.getState().loaded[pon],
|
|
362
|
+
isOverText: (pon, point) => {
|
|
363
|
+
const text = cache.get(pon);
|
|
364
|
+
return text ? glyphAt(text, point) != null : false;
|
|
365
|
+
},
|
|
366
|
+
beginAt: (pon, point) => {
|
|
367
|
+
const text = cache.get(pon);
|
|
368
|
+
if (!text) return false;
|
|
369
|
+
const i = glyphAt(text, point);
|
|
370
|
+
if (i == null) return false;
|
|
371
|
+
recompute({
|
|
372
|
+
anchor: {
|
|
373
|
+
pon,
|
|
374
|
+
glyph: i
|
|
375
|
+
},
|
|
376
|
+
focus: {
|
|
377
|
+
pon,
|
|
378
|
+
glyph: i
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
return true;
|
|
382
|
+
},
|
|
383
|
+
selectWord: (pon, point) => selectSpan(pon, point, "word"),
|
|
384
|
+
selectLine: (pon, point) => selectSpan(pon, point, "line"),
|
|
385
|
+
extendTo: (pon, point) => {
|
|
386
|
+
const cur = ctx.getState().selection;
|
|
387
|
+
if (!cur) return;
|
|
388
|
+
const text = cache.get(pon);
|
|
389
|
+
if (!text) {
|
|
390
|
+
ensurePage(pon);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const i = glyphAt(text, point);
|
|
394
|
+
if (i == null) return;
|
|
395
|
+
recompute({
|
|
396
|
+
anchor: cur.anchor,
|
|
397
|
+
focus: {
|
|
398
|
+
pon,
|
|
399
|
+
glyph: i
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
end: () => commitCbs.forEach((cb) => cb()),
|
|
404
|
+
clear: () => {
|
|
405
|
+
ctx.dispatch({ type: "CLEAR" });
|
|
406
|
+
fireChange();
|
|
407
|
+
},
|
|
408
|
+
snapshot,
|
|
409
|
+
rectsForPage: (pon) => ctx.getState().rects[pon] ?? EMPTY,
|
|
410
|
+
hasSelection: () => ctx.getState().selection != null,
|
|
411
|
+
selectedPages: () => {
|
|
412
|
+
const { rects } = ctx.getState();
|
|
413
|
+
return Object.keys(rects).map(Number).filter((pon) => (rects[pon]?.length ?? 0) > 0);
|
|
414
|
+
},
|
|
415
|
+
onChange: (cb) => {
|
|
416
|
+
changeCbs.add(cb);
|
|
417
|
+
return () => changeCbs.delete(cb);
|
|
418
|
+
},
|
|
419
|
+
onCommit: (cb) => {
|
|
420
|
+
commitCbs.add(cb);
|
|
421
|
+
return () => commitCbs.delete(cb);
|
|
422
|
+
},
|
|
423
|
+
setHighlightVisible: (visible) => ctx.dispatch({
|
|
424
|
+
type: "SET_HIGHLIGHT_HIDDEN",
|
|
425
|
+
hidden: !visible
|
|
426
|
+
}),
|
|
427
|
+
highlightVisible: () => !ctx.getState().highlightHidden
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/handler.ts
|
|
432
|
+
const CURSOR_TOKEN = "selection-text";
|
|
433
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
434
|
+
/**
|
|
435
|
+
* The text-selection pointer handler. It is live only under tools that enable the
|
|
436
|
+
* `'text-select'` tag (the built-in `pointer` tool does; `pan` does not — so
|
|
437
|
+
* switching to pan disables text selection with zero special-casing here).
|
|
438
|
+
*
|
|
439
|
+
* - single click → deselect; records an anchor but selects NOTHING until
|
|
440
|
+
* the pointer moves past the drag threshold (Chrome/Acrobat feel)
|
|
441
|
+
* - single-click + drag → caret selection from the anchor (extends across pages)
|
|
442
|
+
* - double-click → select the word
|
|
443
|
+
* - triple-click → select the visual line
|
|
444
|
+
* - down off-text → deselect, and DON'T capture (so it can't block)
|
|
445
|
+
* - hover → I-beam only when over text, else the pointer cursor
|
|
446
|
+
*/
|
|
447
|
+
function createTextSelectHandler(selection, interaction) {
|
|
448
|
+
let anchor = null;
|
|
449
|
+
let dragging = false;
|
|
450
|
+
return {
|
|
451
|
+
id: "text-select",
|
|
452
|
+
priority: 60,
|
|
453
|
+
enabledFor: (tool) => tool.enables.has("text-select"),
|
|
454
|
+
onDown: (s) => {
|
|
455
|
+
anchor = null;
|
|
456
|
+
dragging = false;
|
|
457
|
+
if (!s.page) return false;
|
|
458
|
+
const { pon, point } = s.page;
|
|
459
|
+
const clicks = s.clickCount ?? 1;
|
|
460
|
+
if (clicks >= 3) {
|
|
461
|
+
selection.selectLine(pon, point);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
if (clicks === 2) {
|
|
465
|
+
selection.selectWord(pon, point);
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
selection.clear();
|
|
469
|
+
if (!selection.isOverText(pon, point)) return false;
|
|
470
|
+
anchor = {
|
|
471
|
+
pon,
|
|
472
|
+
point,
|
|
473
|
+
vx: s.viewport.x,
|
|
474
|
+
vy: s.viewport.y
|
|
475
|
+
};
|
|
476
|
+
return true;
|
|
477
|
+
},
|
|
478
|
+
onMove: (s) => {
|
|
479
|
+
if (!s.page || !anchor) return;
|
|
480
|
+
if (!dragging) {
|
|
481
|
+
if (Math.hypot(s.viewport.x - anchor.vx, s.viewport.y - anchor.vy) < DRAG_THRESHOLD_PX) return;
|
|
482
|
+
dragging = true;
|
|
483
|
+
selection.beginAt(anchor.pon, anchor.point);
|
|
484
|
+
}
|
|
485
|
+
selection.extendTo(s.page.pon, s.page.point);
|
|
486
|
+
},
|
|
487
|
+
onUp: () => {
|
|
488
|
+
anchor = null;
|
|
489
|
+
dragging = false;
|
|
490
|
+
selection.end();
|
|
491
|
+
},
|
|
492
|
+
onHover: (s) => {
|
|
493
|
+
if (!s.page) {
|
|
494
|
+
interaction.setCursor(CURSOR_TOKEN, null);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
selection.ensurePage(s.page.pon);
|
|
498
|
+
const overText = selection.isOverText(s.page.pon, s.page.point);
|
|
499
|
+
interaction.setCursor(CURSOR_TOKEN, overText ? "text" : null, 10);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
//#endregion
|
|
504
|
+
//#region src/reducer.ts
|
|
505
|
+
const initialSelectionState = {
|
|
506
|
+
selection: null,
|
|
507
|
+
rects: {},
|
|
508
|
+
loaded: {},
|
|
509
|
+
highlightHidden: false
|
|
510
|
+
};
|
|
511
|
+
const selectionReducer = (state, action) => {
|
|
512
|
+
switch (action.type) {
|
|
513
|
+
case "PAGE_LOADED": return state.loaded[action.pon] ? state : {
|
|
514
|
+
...state,
|
|
515
|
+
loaded: {
|
|
516
|
+
...state.loaded,
|
|
517
|
+
[action.pon]: true
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
case "SET": return {
|
|
521
|
+
...state,
|
|
522
|
+
selection: action.selection,
|
|
523
|
+
rects: action.rects
|
|
524
|
+
};
|
|
525
|
+
case "CLEAR": return state.selection === null && Object.keys(state.rects).length === 0 ? state : {
|
|
526
|
+
...state,
|
|
527
|
+
selection: null,
|
|
528
|
+
rects: {}
|
|
529
|
+
};
|
|
530
|
+
case "SET_HIGHLIGHT_HIDDEN": return state.highlightHidden === action.hidden ? state : {
|
|
531
|
+
...state,
|
|
532
|
+
highlightHidden: action.hidden
|
|
533
|
+
};
|
|
534
|
+
default: return state;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/types.ts
|
|
539
|
+
const SelectionToken = (0, _embedpdf_core.createCapabilityToken)("selection");
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/selection.plugin.ts
|
|
542
|
+
/**
|
|
543
|
+
* Text selection — document-scoped, requires the interaction hub. In `init` it
|
|
544
|
+
* registers ITS pointer handler with the hub; the hub owns the pointer stream
|
|
545
|
+
* and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection
|
|
546
|
+
* only needs the page coordinate context + the engine's text geometry.
|
|
547
|
+
*/
|
|
548
|
+
const selectionPlugin = () => (0, _embedpdf_core.definePlugin)({
|
|
549
|
+
id: "selection",
|
|
550
|
+
token: SelectionToken,
|
|
551
|
+
scope: "document",
|
|
552
|
+
requires: [_embedpdf_plugin_interaction.InteractionToken],
|
|
553
|
+
initialState: initialSelectionState,
|
|
554
|
+
reduce: selectionReducer,
|
|
555
|
+
capability: createSelectionCapability,
|
|
556
|
+
init: (ctx) => {
|
|
557
|
+
const interaction = ctx.get(_embedpdf_plugin_interaction.InteractionToken);
|
|
558
|
+
const selection = ctx.get(SelectionToken);
|
|
559
|
+
interaction.registerHandler(createTextSelectHandler(selection, interaction));
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
//#endregion
|
|
563
|
+
exports.SelectionToken = SelectionToken;
|
|
564
|
+
exports.buildPageText = buildPageText;
|
|
565
|
+
exports.createSelectionCapability = createSelectionCapability;
|
|
566
|
+
exports.createTextSelectHandler = createTextSelectHandler;
|
|
567
|
+
exports.expandToLine = expandToLine;
|
|
568
|
+
exports.expandToWord = expandToWord;
|
|
569
|
+
exports.glyphAt = glyphAt;
|
|
570
|
+
exports.initialSelectionState = initialSelectionState;
|
|
571
|
+
exports.rectsForRange = rectsForRange;
|
|
572
|
+
exports.selectionPlugin = selectionPlugin;
|
|
573
|
+
exports.selectionReducer = selectionReducer;
|
|
574
|
+
|
|
575
|
+
//# sourceMappingURL=index.cjs.map
|