@embedpdf/plugin-selection 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +364 -30
- package/dist/index.js.map +1 -1
- package/dist/lib/actions.d.ts +10 -1
- package/dist/lib/handlers/text-selection.handler.d.ts +29 -6
- package/dist/lib/selection-plugin.d.ts +13 -0
- package/dist/lib/types.d.ts +18 -0
- package/dist/lib/utils.d.ts +44 -6
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const END_SELECTION = "SELECTION/END_SELECTION";
|
|
|
21
21
|
const CLEAR_SELECTION = "SELECTION/CLEAR_SELECTION";
|
|
22
22
|
const SET_RECTS = "SELECTION/SET_RECTS";
|
|
23
23
|
const SET_SLICES = "SELECTION/SET_SLICES";
|
|
24
|
+
const EVICT_PAGE_GEOMETRY = "SELECTION/EVICT_PAGE_GEOMETRY";
|
|
24
25
|
const RESET = "SELECTION/RESET";
|
|
25
26
|
const initSelectionState = (documentId, state) => ({
|
|
26
27
|
type: INIT_SELECTION_STATE,
|
|
@@ -55,6 +56,10 @@ const setRects = (documentId, allRects) => ({
|
|
|
55
56
|
payload: { documentId, rects: allRects }
|
|
56
57
|
});
|
|
57
58
|
const setSlices = (documentId, slices) => ({ type: SET_SLICES, payload: { documentId, slices } });
|
|
59
|
+
const evictPageGeometry = (documentId, pages) => ({
|
|
60
|
+
type: EVICT_PAGE_GEOMETRY,
|
|
61
|
+
payload: { documentId, pages }
|
|
62
|
+
});
|
|
58
63
|
const initialSelectionDocumentState = {
|
|
59
64
|
geometry: {},
|
|
60
65
|
rects: {},
|
|
@@ -147,6 +152,20 @@ const selectionReducer = (state = initialState, action) => {
|
|
|
147
152
|
if (!docState) return state;
|
|
148
153
|
return updateDocState(state, documentId, { ...docState, slices });
|
|
149
154
|
}
|
|
155
|
+
case EVICT_PAGE_GEOMETRY: {
|
|
156
|
+
const { documentId, pages } = action.payload;
|
|
157
|
+
const docState = state.documents[documentId];
|
|
158
|
+
if (!docState) return state;
|
|
159
|
+
const geometry = { ...docState.geometry };
|
|
160
|
+
const rects = { ...docState.rects };
|
|
161
|
+
const slices = { ...docState.slices };
|
|
162
|
+
for (const p of pages) {
|
|
163
|
+
delete geometry[p];
|
|
164
|
+
delete rects[p];
|
|
165
|
+
delete slices[p];
|
|
166
|
+
}
|
|
167
|
+
return updateDocState(state, documentId, { ...docState, geometry, rects, slices });
|
|
168
|
+
}
|
|
150
169
|
case RESET: {
|
|
151
170
|
const { documentId } = action.payload;
|
|
152
171
|
const docState = state.documents[documentId];
|
|
@@ -197,18 +216,67 @@ function getFormattedSelection(state) {
|
|
|
197
216
|
}
|
|
198
217
|
return result;
|
|
199
218
|
}
|
|
200
|
-
function glyphAt(geo, pt) {
|
|
219
|
+
function glyphAt(geo, pt, toleranceFactor = 1.5) {
|
|
201
220
|
for (const run of geo.runs) {
|
|
202
221
|
const inRun = pt.y >= run.rect.y && pt.y <= run.rect.y + run.rect.height && pt.x >= run.rect.x && pt.x <= run.rect.x + run.rect.width;
|
|
203
222
|
if (!inRun) continue;
|
|
204
|
-
const rel = run.glyphs.findIndex(
|
|
205
|
-
|
|
206
|
-
|
|
223
|
+
const rel = run.glyphs.findIndex((g) => {
|
|
224
|
+
const gx = g.tightX ?? g.x;
|
|
225
|
+
const gy = g.tightY ?? g.y;
|
|
226
|
+
const gw = g.tightWidth ?? g.width;
|
|
227
|
+
const gh = g.tightHeight ?? g.height;
|
|
228
|
+
return pt.x >= gx && pt.x <= gx + gw && pt.y >= gy && pt.y <= gy + gh;
|
|
229
|
+
});
|
|
207
230
|
if (rel !== -1) {
|
|
208
231
|
return run.charStart + rel;
|
|
209
232
|
}
|
|
210
233
|
}
|
|
211
|
-
return -1;
|
|
234
|
+
if (toleranceFactor <= 0) return -1;
|
|
235
|
+
const tolerance = computeTolerance(geo, toleranceFactor);
|
|
236
|
+
const halfTol = tolerance / 2;
|
|
237
|
+
let bestIndex = -1;
|
|
238
|
+
let bestDist = Infinity;
|
|
239
|
+
for (const run of geo.runs) {
|
|
240
|
+
if (pt.y < run.rect.y - halfTol || pt.y > run.rect.y + run.rect.height + halfTol || pt.x < run.rect.x - halfTol || pt.x > run.rect.x + run.rect.width + halfTol) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
for (let i = 0; i < run.glyphs.length; i++) {
|
|
244
|
+
const g = run.glyphs[i];
|
|
245
|
+
if (g.flags === 2) continue;
|
|
246
|
+
const gx = g.tightX ?? g.x;
|
|
247
|
+
const gy = g.tightY ?? g.y;
|
|
248
|
+
const gw = g.tightWidth ?? g.width;
|
|
249
|
+
const gh = g.tightHeight ?? g.height;
|
|
250
|
+
const expandedLeft = gx - halfTol;
|
|
251
|
+
const expandedRight = gx + gw + halfTol;
|
|
252
|
+
const expandedTop = gy - halfTol;
|
|
253
|
+
const expandedBottom = gy + gh + halfTol;
|
|
254
|
+
if (pt.x < expandedLeft || pt.x > expandedRight || pt.y < expandedTop || pt.y > expandedBottom) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const curXdif = Math.min(Math.abs(pt.x - gx), Math.abs(pt.x - (gx + gw)));
|
|
258
|
+
const curYdif = Math.min(Math.abs(pt.y - gy), Math.abs(pt.y - (gy + gh)));
|
|
259
|
+
const dist = curXdif + curYdif;
|
|
260
|
+
if (dist < bestDist) {
|
|
261
|
+
bestDist = dist;
|
|
262
|
+
bestIndex = run.charStart + i;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return bestIndex;
|
|
267
|
+
}
|
|
268
|
+
function computeTolerance(geo, factor) {
|
|
269
|
+
let totalHeight = 0;
|
|
270
|
+
let count = 0;
|
|
271
|
+
for (const run of geo.runs) {
|
|
272
|
+
for (const g of run.glyphs) {
|
|
273
|
+
if (g.flags === 2) continue;
|
|
274
|
+
totalHeight += g.height;
|
|
275
|
+
count++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (count === 0) return 0;
|
|
279
|
+
return totalHeight / count * factor;
|
|
212
280
|
}
|
|
213
281
|
function sliceBounds(sel, geo, page) {
|
|
214
282
|
if (!sel || !geo) return null;
|
|
@@ -221,6 +289,7 @@ function sliceBounds(sel, geo, page) {
|
|
|
221
289
|
}
|
|
222
290
|
function rectsWithinSlice(geo, from, to, merge = true) {
|
|
223
291
|
const textRuns = [];
|
|
292
|
+
const CHAR_DISTANCE_FACTOR = 2.5;
|
|
224
293
|
for (const run of geo.runs) {
|
|
225
294
|
const runStart = run.charStart;
|
|
226
295
|
const runEnd = runStart + run.glyphs.length - 1;
|
|
@@ -230,24 +299,46 @@ function rectsWithinSlice(geo, from, to, merge = true) {
|
|
|
230
299
|
let minX = Infinity, maxX = -Infinity;
|
|
231
300
|
let minY = Infinity, maxY = -Infinity;
|
|
232
301
|
let charCount = 0;
|
|
302
|
+
let widthSum = 0;
|
|
303
|
+
let prevRight = -Infinity;
|
|
304
|
+
const flushSubRun = () => {
|
|
305
|
+
if (minX !== Infinity && charCount > 0) {
|
|
306
|
+
textRuns.push({
|
|
307
|
+
rect: {
|
|
308
|
+
origin: { x: minX, y: minY },
|
|
309
|
+
size: { width: maxX - minX, height: maxY - minY }
|
|
310
|
+
},
|
|
311
|
+
charCount,
|
|
312
|
+
fontSize: run.fontSize
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
minX = Infinity;
|
|
316
|
+
maxX = -Infinity;
|
|
317
|
+
minY = Infinity;
|
|
318
|
+
maxY = -Infinity;
|
|
319
|
+
charCount = 0;
|
|
320
|
+
widthSum = 0;
|
|
321
|
+
prevRight = -Infinity;
|
|
322
|
+
};
|
|
233
323
|
for (let i = sIdx; i <= eIdx; i++) {
|
|
234
324
|
const g = run.glyphs[i];
|
|
235
325
|
if (g.flags === 2) continue;
|
|
326
|
+
if (charCount > 0 && prevRight > -Infinity) {
|
|
327
|
+
const gap = Math.abs(g.x - prevRight);
|
|
328
|
+
const avgWidth = widthSum / charCount;
|
|
329
|
+
if (avgWidth > 0 && gap > CHAR_DISTANCE_FACTOR * avgWidth) {
|
|
330
|
+
flushSubRun();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
236
333
|
minX = Math.min(minX, g.x);
|
|
237
334
|
maxX = Math.max(maxX, g.x + g.width);
|
|
238
335
|
minY = Math.min(minY, g.y);
|
|
239
336
|
maxY = Math.max(maxY, g.y + g.height);
|
|
240
337
|
charCount++;
|
|
338
|
+
widthSum += g.width;
|
|
339
|
+
prevRight = g.x + g.width;
|
|
241
340
|
}
|
|
242
|
-
|
|
243
|
-
textRuns.push({
|
|
244
|
-
rect: {
|
|
245
|
-
origin: { x: minX, y: minY },
|
|
246
|
-
size: { width: maxX - minX, height: maxY - minY }
|
|
247
|
-
},
|
|
248
|
-
charCount
|
|
249
|
-
});
|
|
250
|
-
}
|
|
341
|
+
flushSubRun();
|
|
251
342
|
}
|
|
252
343
|
if (!merge) {
|
|
253
344
|
return textRuns.map((run) => run.rect);
|
|
@@ -289,6 +380,13 @@ function getVerticalOverlap(rect1, rect2) {
|
|
|
289
380
|
return intersectRect.size.height / unionRect.size.height;
|
|
290
381
|
}
|
|
291
382
|
function shouldMergeHorizontalRects(textRun1, textRun2) {
|
|
383
|
+
const FONT_SIZE_RATIO_THRESHOLD = 1.5;
|
|
384
|
+
if (textRun1.fontSize != null && textRun2.fontSize != null && textRun1.fontSize > 0 && textRun2.fontSize > 0) {
|
|
385
|
+
const ratio = Math.max(textRun1.fontSize, textRun2.fontSize) / Math.min(textRun1.fontSize, textRun2.fontSize);
|
|
386
|
+
if (ratio > FONT_SIZE_RATIO_THRESHOLD) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
292
390
|
const VERTICAL_OVERLAP_THRESHOLD = 0.8;
|
|
293
391
|
const rect1 = textRun1.rect;
|
|
294
392
|
const rect2 = textRun2.rect;
|
|
@@ -326,39 +424,178 @@ function mergeAdjacentRects(textRuns) {
|
|
|
326
424
|
}
|
|
327
425
|
return results;
|
|
328
426
|
}
|
|
427
|
+
const VERTICAL_OVERLAP_THRESHOLD_LINE = 0.5;
|
|
428
|
+
function resolveCharIndex(geo, charIndex) {
|
|
429
|
+
for (let r = 0; r < geo.runs.length; r++) {
|
|
430
|
+
const run = geo.runs[r];
|
|
431
|
+
const localIdx = charIndex - run.charStart;
|
|
432
|
+
if (localIdx >= 0 && localIdx < run.glyphs.length) {
|
|
433
|
+
return { runIdx: r, localIdx };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
function isGlyphWordBoundary(flags) {
|
|
439
|
+
return flags === 1 || flags === 2;
|
|
440
|
+
}
|
|
441
|
+
function expandToWordBoundary(geo, charIndex) {
|
|
442
|
+
const resolved = resolveCharIndex(geo, charIndex);
|
|
443
|
+
if (!resolved) return null;
|
|
444
|
+
const totalChars = getTotalCharCount(geo);
|
|
445
|
+
if (totalChars === 0) return null;
|
|
446
|
+
let from = charIndex;
|
|
447
|
+
while (from > 0) {
|
|
448
|
+
const prev = resolveCharIndex(geo, from - 1);
|
|
449
|
+
if (!prev) break;
|
|
450
|
+
if (isGlyphWordBoundary(geo.runs[prev.runIdx].glyphs[prev.localIdx].flags)) break;
|
|
451
|
+
from--;
|
|
452
|
+
}
|
|
453
|
+
let to = charIndex;
|
|
454
|
+
while (to < totalChars - 1) {
|
|
455
|
+
const next = resolveCharIndex(geo, to + 1);
|
|
456
|
+
if (!next) break;
|
|
457
|
+
if (isGlyphWordBoundary(geo.runs[next.runIdx].glyphs[next.localIdx].flags)) break;
|
|
458
|
+
to++;
|
|
459
|
+
}
|
|
460
|
+
return { from, to };
|
|
461
|
+
}
|
|
462
|
+
function expandToLineBoundary(geo, charIndex) {
|
|
463
|
+
const resolved = resolveCharIndex(geo, charIndex);
|
|
464
|
+
if (!resolved) return null;
|
|
465
|
+
const anchorRun = geo.runs[resolved.runIdx];
|
|
466
|
+
const anchorTop = anchorRun.rect.y;
|
|
467
|
+
const anchorBottom = anchorRun.rect.y + anchorRun.rect.height;
|
|
468
|
+
let from = anchorRun.charStart;
|
|
469
|
+
let to = anchorRun.charStart + anchorRun.glyphs.length - 1;
|
|
470
|
+
for (let r = resolved.runIdx - 1; r >= 0; r--) {
|
|
471
|
+
const run = geo.runs[r];
|
|
472
|
+
if (isZeroSizeRun(run)) continue;
|
|
473
|
+
if (!runsOverlapVertically(run.rect.y, run.rect.y + run.rect.height, anchorTop, anchorBottom)) {
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
from = run.charStart;
|
|
477
|
+
}
|
|
478
|
+
for (let r = resolved.runIdx + 1; r < geo.runs.length; r++) {
|
|
479
|
+
const run = geo.runs[r];
|
|
480
|
+
if (isZeroSizeRun(run)) continue;
|
|
481
|
+
if (!runsOverlapVertically(run.rect.y, run.rect.y + run.rect.height, anchorTop, anchorBottom)) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
to = run.charStart + run.glyphs.length - 1;
|
|
485
|
+
}
|
|
486
|
+
return { from, to };
|
|
487
|
+
}
|
|
488
|
+
function isZeroSizeRun(run) {
|
|
489
|
+
return run.rect.width === 0 && run.rect.height === 0;
|
|
490
|
+
}
|
|
491
|
+
function runsOverlapVertically(top1, bottom1, top2, bottom2) {
|
|
492
|
+
const unionHeight = Math.max(bottom1, bottom2) - Math.min(top1, top2);
|
|
493
|
+
const intersectHeight = Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2));
|
|
494
|
+
if (unionHeight === 0) return false;
|
|
495
|
+
return intersectHeight / unionHeight >= VERTICAL_OVERLAP_THRESHOLD_LINE;
|
|
496
|
+
}
|
|
497
|
+
function getTotalCharCount(geo) {
|
|
498
|
+
if (geo.runs.length === 0) return 0;
|
|
499
|
+
const lastRun = geo.runs[geo.runs.length - 1];
|
|
500
|
+
return lastRun.charStart + lastRun.glyphs.length;
|
|
501
|
+
}
|
|
502
|
+
const TRIPLE_CLICK_INTERVAL_MS = 500;
|
|
329
503
|
function createTextSelectionHandler(opts) {
|
|
504
|
+
const minDrag = opts.minDragDistance ?? 3;
|
|
505
|
+
const tolFactor = opts.toleranceFactor ?? 1.5;
|
|
506
|
+
let anchorGlyph = null;
|
|
507
|
+
let anchorPos = null;
|
|
508
|
+
let dragStarted = false;
|
|
509
|
+
let lastDblClickTime = 0;
|
|
510
|
+
function reset() {
|
|
511
|
+
var _a;
|
|
512
|
+
anchorGlyph = null;
|
|
513
|
+
anchorPos = null;
|
|
514
|
+
dragStarted = false;
|
|
515
|
+
(_a = opts.setHasTextAnchor) == null ? void 0 : _a.call(opts, false);
|
|
516
|
+
}
|
|
330
517
|
return {
|
|
331
518
|
onPointerDown: (point, evt, modeId) => {
|
|
332
|
-
var _a;
|
|
519
|
+
var _a, _b;
|
|
333
520
|
if (evt.target === evt.currentTarget) {
|
|
334
521
|
(_a = opts.onEmptySpaceClick) == null ? void 0 : _a.call(opts, modeId);
|
|
335
522
|
}
|
|
336
523
|
if (!opts.isEnabled(modeId)) return;
|
|
337
|
-
|
|
524
|
+
const now = Date.now();
|
|
525
|
+
if (lastDblClickTime === 0 || now - lastDblClickTime >= TRIPLE_CLICK_INTERVAL_MS) {
|
|
526
|
+
opts.onClear(modeId);
|
|
527
|
+
}
|
|
338
528
|
const geo = opts.getGeometry();
|
|
339
|
-
if (geo)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
529
|
+
if (!geo) return;
|
|
530
|
+
const g = glyphAt(geo, point, tolFactor);
|
|
531
|
+
if (g !== -1) {
|
|
532
|
+
anchorGlyph = g;
|
|
533
|
+
anchorPos = point;
|
|
534
|
+
dragStarted = false;
|
|
535
|
+
(_b = opts.setHasTextAnchor) == null ? void 0 : _b.call(opts, true);
|
|
344
536
|
}
|
|
345
537
|
},
|
|
346
538
|
onPointerMove: (point, _evt, modeId) => {
|
|
347
539
|
if (!opts.isEnabled(modeId)) return;
|
|
348
540
|
const geo = opts.getGeometry();
|
|
349
|
-
if (geo)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
541
|
+
if (!geo) return;
|
|
542
|
+
const g = glyphAt(geo, point, tolFactor);
|
|
543
|
+
opts.setCursor(g !== -1 ? "text" : null);
|
|
544
|
+
if (anchorGlyph !== null && anchorPos && !dragStarted) {
|
|
545
|
+
const dx = point.x - anchorPos.x;
|
|
546
|
+
const dy = point.y - anchorPos.y;
|
|
547
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
548
|
+
if (dist >= minDrag) {
|
|
549
|
+
dragStarted = true;
|
|
550
|
+
opts.onBegin(anchorGlyph, modeId);
|
|
551
|
+
if (g !== -1) {
|
|
552
|
+
opts.onUpdate(g, modeId);
|
|
553
|
+
}
|
|
354
554
|
}
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (opts.isSelecting() && g !== -1) {
|
|
558
|
+
opts.onUpdate(g, modeId);
|
|
355
559
|
}
|
|
356
560
|
},
|
|
357
561
|
onPointerUp: (_point, _evt, modeId) => {
|
|
562
|
+
if (!opts.isEnabled(modeId)) {
|
|
563
|
+
reset();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (dragStarted) {
|
|
567
|
+
opts.onEnd(modeId);
|
|
568
|
+
}
|
|
569
|
+
reset();
|
|
570
|
+
},
|
|
571
|
+
onDoubleClick: (point, _evt, modeId) => {
|
|
572
|
+
var _a;
|
|
573
|
+
if (!opts.isEnabled(modeId)) return;
|
|
574
|
+
const geo = opts.getGeometry();
|
|
575
|
+
if (!geo) return;
|
|
576
|
+
const g = glyphAt(geo, point, tolFactor);
|
|
577
|
+
if (g === -1) return;
|
|
578
|
+
lastDblClickTime = Date.now();
|
|
579
|
+
(_a = opts.onWordSelect) == null ? void 0 : _a.call(opts, g, modeId);
|
|
580
|
+
},
|
|
581
|
+
onClick: (point, _evt, modeId) => {
|
|
582
|
+
var _a;
|
|
358
583
|
if (!opts.isEnabled(modeId)) return;
|
|
359
|
-
|
|
584
|
+
if (lastDblClickTime === 0) return;
|
|
585
|
+
const now = Date.now();
|
|
586
|
+
if (now - lastDblClickTime > TRIPLE_CLICK_INTERVAL_MS) {
|
|
587
|
+
lastDblClickTime = 0;
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
lastDblClickTime = 0;
|
|
591
|
+
const geo = opts.getGeometry();
|
|
592
|
+
if (!geo) return;
|
|
593
|
+
const g = glyphAt(geo, point, tolFactor);
|
|
594
|
+
if (g === -1) return;
|
|
595
|
+
(_a = opts.onLineSelect) == null ? void 0 : _a.call(opts, g, modeId);
|
|
360
596
|
},
|
|
361
597
|
onHandlerActiveEnd: (modeId) => {
|
|
598
|
+
reset();
|
|
362
599
|
if (!opts.isEnabled(modeId)) return;
|
|
363
600
|
opts.onClear(modeId);
|
|
364
601
|
}
|
|
@@ -420,8 +657,10 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
420
657
|
this.enabledModesPerDoc = /* @__PURE__ */ new Map();
|
|
421
658
|
this.selecting = /* @__PURE__ */ new Map();
|
|
422
659
|
this.anchor = /* @__PURE__ */ new Map();
|
|
660
|
+
this.hasTextAnchor = /* @__PURE__ */ new Map();
|
|
423
661
|
this.marqueePage = /* @__PURE__ */ new Map();
|
|
424
662
|
this.pageCallbacks = /* @__PURE__ */ new Map();
|
|
663
|
+
this.geoAccessOrder = /* @__PURE__ */ new Map();
|
|
425
664
|
this.menuPlacement$ = createScopedEmitter((documentId, placement) => ({ documentId, placement }));
|
|
426
665
|
this.selChange$ = createScopedEmitter((documentId, selection) => ({
|
|
427
666
|
documentId,
|
|
@@ -520,14 +759,18 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
520
759
|
])
|
|
521
760
|
);
|
|
522
761
|
this.pageCallbacks.set(documentId, /* @__PURE__ */ new Map());
|
|
762
|
+
this.geoAccessOrder.set(documentId, []);
|
|
523
763
|
this.selecting.set(documentId, false);
|
|
524
764
|
this.anchor.set(documentId, void 0);
|
|
765
|
+
this.hasTextAnchor.set(documentId, false);
|
|
525
766
|
}
|
|
526
767
|
onDocumentClosed(documentId) {
|
|
527
768
|
this.dispatch(cleanupSelectionState(documentId));
|
|
528
769
|
this.enabledModesPerDoc.delete(documentId);
|
|
529
770
|
this.pageCallbacks.delete(documentId);
|
|
771
|
+
this.geoAccessOrder.delete(documentId);
|
|
530
772
|
this.selecting.delete(documentId);
|
|
773
|
+
this.hasTextAnchor.delete(documentId);
|
|
531
774
|
this.anchor.delete(documentId);
|
|
532
775
|
this.marqueePage.delete(documentId);
|
|
533
776
|
this.selChange$.clearScope(documentId);
|
|
@@ -659,6 +902,22 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
659
902
|
rects: selectRectsForPage(docState, pageIndex),
|
|
660
903
|
boundingRect: selectBoundingRectForPage(docState, pageIndex)
|
|
661
904
|
});
|
|
905
|
+
geoTask.wait((geo) => {
|
|
906
|
+
const currentState = this.getDocumentState(documentId);
|
|
907
|
+
const sel = currentState.selection;
|
|
908
|
+
if (!sel || pageIndex < sel.start.page || pageIndex > sel.end.page) return;
|
|
909
|
+
const sb = sliceBounds(sel, geo, pageIndex);
|
|
910
|
+
if (!sb) return;
|
|
911
|
+
const pageRects = rectsWithinSlice(geo, sb.from, sb.to);
|
|
912
|
+
this.dispatch(setRects(documentId, { ...currentState.rects, [pageIndex]: pageRects }));
|
|
913
|
+
this.dispatch(
|
|
914
|
+
setSlices(documentId, {
|
|
915
|
+
...currentState.slices,
|
|
916
|
+
[pageIndex]: { start: sb.from, count: sb.to - sb.from + 1 }
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
this.notifyPage(documentId, pageIndex);
|
|
920
|
+
}, ignore);
|
|
662
921
|
const textHandler = createTextSelectionHandler({
|
|
663
922
|
getGeometry: () => this.getDocumentState(documentId).geometry[pageIndex],
|
|
664
923
|
isEnabled: (modeId) => {
|
|
@@ -672,7 +931,12 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
672
931
|
onClear: (modeId) => this.clearSelection(documentId, modeId),
|
|
673
932
|
isSelecting: () => this.selecting.get(documentId) ?? false,
|
|
674
933
|
setCursor: (cursor) => cursor ? interactionScope.setCursor("selection-text", cursor, 10) : interactionScope.removeCursor("selection-text"),
|
|
675
|
-
onEmptySpaceClick: (modeId) => this.emptySpaceClick$.emit(documentId, { pageIndex, modeId })
|
|
934
|
+
onEmptySpaceClick: (modeId) => this.emptySpaceClick$.emit(documentId, { pageIndex, modeId }),
|
|
935
|
+
onWordSelect: (g, modeId) => this.selectWord(documentId, pageIndex, g, modeId),
|
|
936
|
+
onLineSelect: (g, modeId) => this.selectLine(documentId, pageIndex, g, modeId),
|
|
937
|
+
setHasTextAnchor: (active) => this.hasTextAnchor.set(documentId, active),
|
|
938
|
+
minDragDistance: this.config.minSelectionDragDistance,
|
|
939
|
+
toleranceFactor: this.config.toleranceFactor
|
|
676
940
|
});
|
|
677
941
|
const unregisterHandlers = this.interactionManagerCapability.registerAlways({
|
|
678
942
|
scope: { type: "page", documentId, pageIndex },
|
|
@@ -739,7 +1003,7 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
739
1003
|
const config = (_a2 = this.enabledModesPerDoc.get(documentId)) == null ? void 0 : _a2.get(modeId);
|
|
740
1004
|
return (config == null ? void 0 : config.enableMarquee) === true;
|
|
741
1005
|
},
|
|
742
|
-
isTextSelecting: () => this.selecting.get(documentId) ?? false,
|
|
1006
|
+
isTextSelecting: () => (this.selecting.get(documentId) ?? false) || (this.hasTextAnchor.get(documentId) ?? false),
|
|
743
1007
|
onBegin: (pos, modeId) => this.beginMarquee(documentId, pageIndex, pos, modeId),
|
|
744
1008
|
onChange: (rect, modeId) => {
|
|
745
1009
|
this.updateMarquee(documentId, pageIndex, rect, modeId);
|
|
@@ -886,15 +1150,46 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
886
1150
|
const task = this.engine.getPageGeometry(coreDoc.document, page);
|
|
887
1151
|
task.wait((geo) => {
|
|
888
1152
|
this.dispatch(cachePageGeometry(documentId, pageIdx, geo));
|
|
1153
|
+
this.touchGeometry(documentId, pageIdx);
|
|
889
1154
|
}, ignore);
|
|
890
1155
|
return task;
|
|
891
1156
|
}
|
|
892
1157
|
/* ── geometry cache ───────────────────────────────────── */
|
|
893
1158
|
getOrLoadGeometry(documentId, pageIdx) {
|
|
894
1159
|
const cached = this.getDocumentState(documentId).geometry[pageIdx];
|
|
895
|
-
if (cached)
|
|
1160
|
+
if (cached) {
|
|
1161
|
+
this.touchGeometry(documentId, pageIdx);
|
|
1162
|
+
return PdfTaskHelper.resolve(cached);
|
|
1163
|
+
}
|
|
896
1164
|
return this.getNewPageGeometryAndCache(documentId, pageIdx);
|
|
897
1165
|
}
|
|
1166
|
+
/* ── geometry LRU eviction ──────────────────────────────── */
|
|
1167
|
+
touchGeometry(documentId, pageIdx) {
|
|
1168
|
+
const order = this.geoAccessOrder.get(documentId);
|
|
1169
|
+
if (!order) return;
|
|
1170
|
+
const idx = order.indexOf(pageIdx);
|
|
1171
|
+
if (idx > -1) order.splice(idx, 1);
|
|
1172
|
+
order.push(pageIdx);
|
|
1173
|
+
this.evictGeometryIfNeeded(documentId);
|
|
1174
|
+
}
|
|
1175
|
+
evictGeometryIfNeeded(documentId) {
|
|
1176
|
+
const max = this.config.maxCachedGeometries ?? 50;
|
|
1177
|
+
const order = this.geoAccessOrder.get(documentId);
|
|
1178
|
+
if (!order || order.length <= max) return;
|
|
1179
|
+
const pinned = this.pageCallbacks.get(documentId);
|
|
1180
|
+
const toEvict = [];
|
|
1181
|
+
while (order.length - toEvict.length > max) {
|
|
1182
|
+
const candidate = order.find((p) => !toEvict.includes(p) && !(pinned == null ? void 0 : pinned.has(p)));
|
|
1183
|
+
if (candidate === void 0) break;
|
|
1184
|
+
toEvict.push(candidate);
|
|
1185
|
+
}
|
|
1186
|
+
if (toEvict.length === 0) return;
|
|
1187
|
+
for (const p of toEvict) {
|
|
1188
|
+
const idx = order.indexOf(p);
|
|
1189
|
+
if (idx > -1) order.splice(idx, 1);
|
|
1190
|
+
}
|
|
1191
|
+
this.dispatch(evictPageGeometry(documentId, toEvict));
|
|
1192
|
+
}
|
|
898
1193
|
/* ── selection state updates ───────────────────────────── */
|
|
899
1194
|
beginSelection(documentId, page, index, modeId) {
|
|
900
1195
|
this.selecting.set(documentId, true);
|
|
@@ -918,6 +1213,43 @@ const _SelectionPlugin = class _SelectionPlugin extends BasePlugin {
|
|
|
918
1213
|
this.emitMenuPlacement(documentId, null);
|
|
919
1214
|
this.notifyAllPages(documentId);
|
|
920
1215
|
}
|
|
1216
|
+
selectWord(documentId, page, charIndex, modeId) {
|
|
1217
|
+
const geo = this.getDocumentState(documentId).geometry[page];
|
|
1218
|
+
if (!geo) return;
|
|
1219
|
+
const bounds = expandToWordBoundary(geo, charIndex);
|
|
1220
|
+
if (!bounds) return;
|
|
1221
|
+
this.applyInstantSelection(documentId, page, bounds.from, bounds.to, modeId);
|
|
1222
|
+
}
|
|
1223
|
+
selectLine(documentId, page, charIndex, modeId) {
|
|
1224
|
+
const geo = this.getDocumentState(documentId).geometry[page];
|
|
1225
|
+
if (!geo) return;
|
|
1226
|
+
const bounds = expandToLineBoundary(geo, charIndex);
|
|
1227
|
+
if (!bounds) return;
|
|
1228
|
+
this.applyInstantSelection(documentId, page, bounds.from, bounds.to, modeId);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Set a selection range without going through the drag begin/update/end flow.
|
|
1232
|
+
* Used by double-click (word) and triple-click (line) selection.
|
|
1233
|
+
*/
|
|
1234
|
+
applyInstantSelection(documentId, page, from, to, modeId) {
|
|
1235
|
+
const range = {
|
|
1236
|
+
start: { page, index: from },
|
|
1237
|
+
end: { page, index: to }
|
|
1238
|
+
};
|
|
1239
|
+
this.selecting.set(documentId, false);
|
|
1240
|
+
this.anchor.set(documentId, void 0);
|
|
1241
|
+
this.dispatch(startSelection(documentId));
|
|
1242
|
+
this.dispatch(setSelection(documentId, range));
|
|
1243
|
+
this.updateRectsAndSlices(documentId, range);
|
|
1244
|
+
this.dispatch(endSelection(documentId));
|
|
1245
|
+
this.selChange$.emit(documentId, range);
|
|
1246
|
+
this.beginSelection$.emit(documentId, { page, index: from, modeId });
|
|
1247
|
+
this.endSelection$.emit(documentId, { modeId });
|
|
1248
|
+
for (let p = range.start.page; p <= range.end.page; p++) {
|
|
1249
|
+
this.notifyPage(documentId, p);
|
|
1250
|
+
}
|
|
1251
|
+
this.recalculateMenuPlacement(documentId);
|
|
1252
|
+
}
|
|
921
1253
|
updateSelection(documentId, page, index, modeId) {
|
|
922
1254
|
if (!this.selecting.get(documentId) || !this.anchor.get(documentId)) return;
|
|
923
1255
|
const a = this.anchor.get(documentId);
|
|
@@ -1042,6 +1374,8 @@ export {
|
|
|
1042
1374
|
SELECTION_PLUGIN_ID,
|
|
1043
1375
|
SelectionPlugin,
|
|
1044
1376
|
SelectionPluginPackage,
|
|
1377
|
+
expandToLineBoundary,
|
|
1378
|
+
expandToWordBoundary,
|
|
1045
1379
|
getVerticalOverlap,
|
|
1046
1380
|
glyphAt,
|
|
1047
1381
|
manifest,
|