kviewer 0.0.9 → 0.0.10

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.
Files changed (26) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/annotation/engine/editor/editor.d.ts +6 -3
  3. package/dist/runtime/annotation/engine/editor/editor.js +22 -9
  4. package/dist/runtime/annotation/engine/editor/selector.d.ts +9 -1
  5. package/dist/runtime/annotation/engine/editor/selector.js +72 -30
  6. package/dist/runtime/annotation/engine/painter.d.ts +2 -1
  7. package/dist/runtime/annotation/engine/painter.js +16 -4
  8. package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
  9. package/dist/runtime/components/PdfPage.vue +51 -18
  10. package/dist/runtime/components/PdfPage.vue.d.ts +6 -1
  11. package/dist/runtime/components/ToolButton.d.vue.ts +4 -2
  12. package/dist/runtime/components/ToolButton.vue.d.ts +4 -2
  13. package/dist/runtime/components/Viewer.vue +213 -164
  14. package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
  15. package/dist/runtime/components/tools/ActionTools.vue +4 -5
  16. package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
  17. package/dist/runtime/composables/useAnnotationEngine.js +4 -1
  18. package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
  19. package/dist/runtime/composables/useAnnotationHistory.js +10 -3
  20. package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
  21. package/dist/runtime/composables/useInertiaPanzoom.js +472 -0
  22. package/dist/runtime/composables/usePageVirtualization.d.ts +3 -0
  23. package/dist/runtime/composables/usePageVirtualization.js +23 -0
  24. package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
  25. package/dist/runtime/composables/useViewerSearch.js +15 -5
  26. package/package.json +1 -1
@@ -0,0 +1,40 @@
1
+ export interface InertiaPanzoomOptions {
2
+ minScale?: number;
3
+ maxScale?: number;
4
+ /** Minimum pointer movement (px) before axis direction is committed. */
5
+ axisLockThreshold?: number;
6
+ /** Vertical must exceed horizontal by this ratio to lock to Y-axis. */
7
+ axisLockRatio?: number;
8
+ }
9
+ export interface ScrollToElementOptions {
10
+ /** Vertical alignment within the viewport. Default: 'start'. */
11
+ block?: 'start' | 'center';
12
+ /** Animate to the target with inertia, or snap instantly. Default: true. */
13
+ animate?: boolean;
14
+ }
15
+ export interface Transform {
16
+ scale: number;
17
+ tx: number;
18
+ ty: number;
19
+ }
20
+ export interface InertiaPanzoomHandle {
21
+ /** Bind gesture handlers to the given element (its parent becomes the viewport). */
22
+ attach: (target: HTMLElement) => void;
23
+ /** Remove handlers and clear inline styles. */
24
+ detach: () => void;
25
+ /** Reset transform and cancel any in-flight momentum. */
26
+ reset: () => void;
27
+ /** Cancel in-flight momentum without changing transform. */
28
+ stop: () => void;
29
+ getScale: () => number;
30
+ getTransform: () => Transform;
31
+ /** Animate the view so `elem` is at the top (or center) of the viewport. */
32
+ scrollToElement: (elem: HTMLElement, opts?: ScrollToElementOptions) => void;
33
+ /** Pan by a delta in viewport pixels (clamped to bounds). */
34
+ scrollBy: (dx: number, dy: number) => void;
35
+ /** Enable/disable pan+pinch gestures (wheel/scrollbar/keyboard still work). */
36
+ setGesturesEnabled: (enabled: boolean) => void;
37
+ /** Subscribe to transform updates. Returns an unsubscribe function. */
38
+ onChange: (cb: (t: Transform) => void) => () => void;
39
+ }
40
+ export declare function useInertiaPanzoom(options?: InertiaPanzoomOptions): InertiaPanzoomHandle;
@@ -0,0 +1,472 @@
1
+ import { onBeforeUnmount } from "vue";
2
+ const DECELERATION = 0.998;
3
+ const RUBBER_BAND_C = 0.55;
4
+ const BOUNCE_BACK_RATE = 0.04;
5
+ const BOUNCE_VELOCITY_DECAY = 0.04;
6
+ const MIN_VELOCITY = 0.01;
7
+ export function useInertiaPanzoom(options = {}) {
8
+ const {
9
+ minScale = 0.5,
10
+ maxScale = 4,
11
+ axisLockThreshold = 10,
12
+ axisLockRatio = 2
13
+ } = options;
14
+ let controller = null;
15
+ function attach(target) {
16
+ detach();
17
+ const parent = target.parentElement;
18
+ if (!parent) return;
19
+ let scale = 1;
20
+ let tx = 0;
21
+ let ty = 0;
22
+ let gesturesEnabled = true;
23
+ const pointers = /* @__PURE__ */ new Map();
24
+ let pinchStart = null;
25
+ let panStart = null;
26
+ let lastMoveTime = 0;
27
+ let lastMoveX = 0;
28
+ let lastMoveY = 0;
29
+ let vx = 0;
30
+ let vy = 0;
31
+ let momentumRaf = 0;
32
+ let lastStepTime = 0;
33
+ const subscribers = /* @__PURE__ */ new Set();
34
+ target.style.transformOrigin = "0 0";
35
+ target.style.touchAction = "none";
36
+ parent.style.touchAction = "none";
37
+ target.style.userSelect = "none";
38
+ let willChangeActive = false;
39
+ function setWillChangeActive(active) {
40
+ if (willChangeActive === active) return;
41
+ willChangeActive = active;
42
+ target.style.willChange = active ? "transform" : "";
43
+ }
44
+ const BAR_SIZE = 10;
45
+ const vBar = document.createElement("div");
46
+ vBar.setAttribute("data-inertia-scrollbar", "y");
47
+ vBar.style.cssText = `position:absolute;right:2px;top:0;width:${BAR_SIZE}px;background:rgba(0,0,0,0.35);border-radius:${BAR_SIZE / 2}px;opacity:0;transition:opacity 300ms,background-color 150ms;z-index:10;touch-action:none;cursor:grab;`;
48
+ const hBar = document.createElement("div");
49
+ hBar.setAttribute("data-inertia-scrollbar", "x");
50
+ hBar.style.cssText = `position:absolute;bottom:2px;left:0;height:${BAR_SIZE}px;background:rgba(0,0,0,0.35);border-radius:${BAR_SIZE / 2}px;opacity:0;transition:opacity 300ms,background-color 150ms;z-index:10;touch-action:none;cursor:grab;`;
51
+ parent.appendChild(vBar);
52
+ parent.appendChild(hBar);
53
+ let hideTimer = 0;
54
+ let barDrag = null;
55
+ function updateIndicators() {
56
+ const pw = parent.clientWidth;
57
+ const ph = parent.clientHeight;
58
+ const cw = target.scrollWidth * scale;
59
+ const ch = target.scrollHeight * scale;
60
+ if (ch > ph) {
61
+ const thumbH = Math.max(30, ph / ch * ph);
62
+ const scrollable = ch - ph;
63
+ const st = Math.max(0, Math.min(scrollable, -ty));
64
+ vBar.style.height = `${thumbH}px`;
65
+ vBar.style.top = `${st / scrollable * (ph - thumbH)}px`;
66
+ vBar.style.opacity = "1";
67
+ vBar.style.display = "";
68
+ } else {
69
+ vBar.style.opacity = "0";
70
+ vBar.style.display = "none";
71
+ }
72
+ if (cw > pw) {
73
+ const thumbW = Math.max(30, pw / cw * pw);
74
+ const scrollable = cw - pw;
75
+ const sl = Math.max(0, Math.min(scrollable, -tx));
76
+ hBar.style.width = `${thumbW}px`;
77
+ hBar.style.left = `${sl / scrollable * (pw - thumbW)}px`;
78
+ hBar.style.opacity = "1";
79
+ hBar.style.display = "";
80
+ } else {
81
+ hBar.style.opacity = "0";
82
+ hBar.style.display = "none";
83
+ }
84
+ if (barDrag) return;
85
+ clearTimeout(hideTimer);
86
+ hideTimer = window.setTimeout(() => {
87
+ vBar.style.opacity = "0";
88
+ hBar.style.opacity = "0";
89
+ }, 800);
90
+ }
91
+ function onBarDown(axis) {
92
+ return (e) => {
93
+ e.stopPropagation();
94
+ e.preventDefault();
95
+ stopMomentum();
96
+ const pw = parent.clientWidth;
97
+ const ph = parent.clientHeight;
98
+ const cw = target.scrollWidth * scale;
99
+ const ch = target.scrollHeight * scale;
100
+ if (axis === "y") {
101
+ if (ch <= ph) return;
102
+ const thumbH = Math.max(30, ph / ch * ph);
103
+ const trackH = ph - thumbH;
104
+ const scrollable = ch - ph;
105
+ barDrag = {
106
+ axis: "y",
107
+ startPointer: e.clientY,
108
+ startT: ty,
109
+ ratio: scrollable / trackH,
110
+ pointerId: e.pointerId
111
+ };
112
+ vBar.style.background = "rgba(0,0,0,0.55)";
113
+ } else {
114
+ if (cw <= pw) return;
115
+ const thumbW = Math.max(30, pw / cw * pw);
116
+ const trackW = pw - thumbW;
117
+ const scrollable = cw - pw;
118
+ barDrag = {
119
+ axis: "x",
120
+ startPointer: e.clientX,
121
+ startT: tx,
122
+ ratio: scrollable / trackW,
123
+ pointerId: e.pointerId
124
+ };
125
+ hBar.style.background = "rgba(0,0,0,0.55)";
126
+ }
127
+ e.target.setPointerCapture?.(e.pointerId);
128
+ };
129
+ }
130
+ function onBarMove(e) {
131
+ if (!barDrag || e.pointerId !== barDrag.pointerId) return;
132
+ e.stopPropagation();
133
+ if (barDrag.axis === "y") {
134
+ const dy = e.clientY - barDrag.startPointer;
135
+ const b = getBounds();
136
+ ty = Math.max(b.minY, Math.min(b.maxY, barDrag.startT - dy * barDrag.ratio));
137
+ } else {
138
+ const dx = e.clientX - barDrag.startPointer;
139
+ const b = getBounds();
140
+ tx = Math.max(b.minX, Math.min(b.maxX, barDrag.startT - dx * barDrag.ratio));
141
+ }
142
+ apply();
143
+ }
144
+ function onBarUp(e) {
145
+ if (!barDrag || e.pointerId !== barDrag.pointerId) return;
146
+ e.stopPropagation();
147
+ vBar.style.background = "rgba(0,0,0,0.35)";
148
+ hBar.style.background = "rgba(0,0,0,0.35)";
149
+ barDrag = null;
150
+ updateIndicators();
151
+ }
152
+ vBar.addEventListener("pointerdown", onBarDown("y"));
153
+ hBar.addEventListener("pointerdown", onBarDown("x"));
154
+ vBar.addEventListener("pointermove", onBarMove);
155
+ hBar.addEventListener("pointermove", onBarMove);
156
+ vBar.addEventListener("pointerup", onBarUp);
157
+ hBar.addEventListener("pointerup", onBarUp);
158
+ vBar.addEventListener("pointercancel", onBarUp);
159
+ hBar.addEventListener("pointercancel", onBarUp);
160
+ function apply() {
161
+ target.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
162
+ updateIndicators();
163
+ if (subscribers.size > 0) {
164
+ const t = { scale, tx, ty };
165
+ subscribers.forEach((cb) => cb(t));
166
+ }
167
+ }
168
+ function scrollToElement(elem, opts) {
169
+ const block = opts?.block ?? "start";
170
+ const animate = opts?.animate ?? true;
171
+ const elemRect = elem.getBoundingClientRect();
172
+ const parentRect = parent.getBoundingClientRect();
173
+ const targetOffset = block === "center" ? (parentRect.height - elemRect.height) / 2 : 0;
174
+ const delta = elemRect.top - parentRect.top - targetOffset;
175
+ let newTy = ty - delta;
176
+ const b = getBounds();
177
+ newTy = Math.max(b.minY, Math.min(b.maxY, newTy));
178
+ stopMomentum();
179
+ if (!animate) {
180
+ ty = newTy;
181
+ apply();
182
+ return;
183
+ }
184
+ const startTy = ty;
185
+ const startTime = performance.now();
186
+ const duration = Math.min(600, 200 + Math.abs(newTy - startTy) * 0.5);
187
+ setWillChangeActive(true);
188
+ function frame() {
189
+ const t = Math.min(1, (performance.now() - startTime) / duration);
190
+ const eased = 1 - Math.pow(1 - t, 3);
191
+ ty = startTy + (newTy - startTy) * eased;
192
+ apply();
193
+ if (t < 1) {
194
+ momentumRaf = requestAnimationFrame(frame);
195
+ } else {
196
+ momentumRaf = 0;
197
+ setWillChangeActive(false);
198
+ }
199
+ }
200
+ momentumRaf = requestAnimationFrame(frame);
201
+ }
202
+ function getBounds() {
203
+ const pw = parent.clientWidth;
204
+ const ph = parent.clientHeight;
205
+ const cw = target.scrollWidth * scale;
206
+ const ch = target.scrollHeight * scale;
207
+ let minX, maxX, minY, maxY;
208
+ if (cw <= pw) {
209
+ minX = maxX = (pw - cw) / 2;
210
+ } else {
211
+ minX = pw - cw;
212
+ maxX = 0;
213
+ }
214
+ if (ch <= ph) {
215
+ minY = maxY = (ph - ch) / 2;
216
+ } else {
217
+ minY = ph - ch;
218
+ maxY = 0;
219
+ }
220
+ return { minX, maxX, minY, maxY };
221
+ }
222
+ function rubberBandAxis(v, min, max, dim) {
223
+ if (v < min) return min - (1 - 1 / ((min - v) * RUBBER_BAND_C / dim + 1)) * dim;
224
+ if (v > max) return max + (1 - 1 / ((v - max) * RUBBER_BAND_C / dim + 1)) * dim;
225
+ return v;
226
+ }
227
+ function stopMomentum() {
228
+ if (momentumRaf) {
229
+ cancelAnimationFrame(momentumRaf);
230
+ momentumRaf = 0;
231
+ }
232
+ }
233
+ function startMomentum() {
234
+ stopMomentum();
235
+ lastStepTime = performance.now();
236
+ function step() {
237
+ const now = performance.now();
238
+ const dt = Math.min(32, now - lastStepTime);
239
+ lastStepTime = now;
240
+ const b = getBounds();
241
+ if (tx < b.minX) {
242
+ tx += (b.minX - tx) * (1 - Math.exp(-BOUNCE_BACK_RATE * dt));
243
+ vx *= Math.exp(-BOUNCE_VELOCITY_DECAY * dt);
244
+ } else if (tx > b.maxX) {
245
+ tx += (b.maxX - tx) * (1 - Math.exp(-BOUNCE_BACK_RATE * dt));
246
+ vx *= Math.exp(-BOUNCE_VELOCITY_DECAY * dt);
247
+ } else {
248
+ vx *= Math.pow(DECELERATION, dt);
249
+ tx += vx * dt;
250
+ }
251
+ if (ty < b.minY) {
252
+ ty += (b.minY - ty) * (1 - Math.exp(-BOUNCE_BACK_RATE * dt));
253
+ vy *= Math.exp(-BOUNCE_VELOCITY_DECAY * dt);
254
+ } else if (ty > b.maxY) {
255
+ ty += (b.maxY - ty) * (1 - Math.exp(-BOUNCE_BACK_RATE * dt));
256
+ vy *= Math.exp(-BOUNCE_VELOCITY_DECAY * dt);
257
+ } else {
258
+ vy *= Math.pow(DECELERATION, dt);
259
+ ty += vy * dt;
260
+ }
261
+ apply();
262
+ const stopped = Math.abs(vx) < MIN_VELOCITY && Math.abs(vy) < MIN_VELOCITY;
263
+ const inBounds = tx >= b.minX - 0.5 && tx <= b.maxX + 0.5 && ty >= b.minY - 0.5 && ty <= b.maxY + 0.5;
264
+ if (stopped && inBounds) {
265
+ tx = Math.max(b.minX, Math.min(b.maxX, tx));
266
+ ty = Math.max(b.minY, Math.min(b.maxY, ty));
267
+ vx = vy = 0;
268
+ apply();
269
+ momentumRaf = 0;
270
+ setWillChangeActive(false);
271
+ return;
272
+ }
273
+ momentumRaf = requestAnimationFrame(step);
274
+ }
275
+ momentumRaf = requestAnimationFrame(step);
276
+ }
277
+ function onPenFilter(e) {
278
+ if (e instanceof PointerEvent && e.pointerType === "pen") {
279
+ e.stopPropagation();
280
+ }
281
+ }
282
+ function onDown(e) {
283
+ if (e.pointerType === "pen") return;
284
+ if (!gesturesEnabled) return;
285
+ stopMomentum();
286
+ setWillChangeActive(true);
287
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
288
+ if (pointers.size === 2) {
289
+ const pts = [...pointers.values()];
290
+ pinchStart = {
291
+ dist: Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y),
292
+ midX: (pts[0].x + pts[1].x) / 2,
293
+ midY: (pts[0].y + pts[1].y) / 2,
294
+ s: scale,
295
+ tx,
296
+ ty
297
+ };
298
+ panStart = null;
299
+ } else if (pointers.size === 1) {
300
+ panStart = { x: e.clientX, y: e.clientY, tx, ty, axisLock: "none" };
301
+ lastMoveTime = performance.now();
302
+ lastMoveX = e.clientX;
303
+ lastMoveY = e.clientY;
304
+ vx = vy = 0;
305
+ }
306
+ }
307
+ function onMove(e) {
308
+ if (!pointers.has(e.pointerId)) return;
309
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
310
+ if (pointers.size === 2 && pinchStart) {
311
+ e.preventDefault();
312
+ const pts = [...pointers.values()];
313
+ const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
314
+ const midX = (pts[0].x + pts[1].x) / 2;
315
+ const midY = (pts[0].y + pts[1].y) / 2;
316
+ const newScale = Math.max(minScale, Math.min(maxScale, pinchStart.s * (dist / pinchStart.dist)));
317
+ const rect = parent.getBoundingClientRect();
318
+ const focalX = (pinchStart.midX - rect.left - pinchStart.tx) / pinchStart.s;
319
+ const focalY = (pinchStart.midY - rect.top - pinchStart.ty) / pinchStart.s;
320
+ tx = midX - rect.left - newScale * focalX;
321
+ ty = midY - rect.top - newScale * focalY;
322
+ scale = newScale;
323
+ apply();
324
+ } else if (pointers.size === 1 && panStart) {
325
+ e.preventDefault();
326
+ const dx = e.clientX - panStart.x;
327
+ const dy = e.clientY - panStart.y;
328
+ if (panStart.axisLock === "none") {
329
+ const absDx = Math.abs(dx);
330
+ const absDy = Math.abs(dy);
331
+ if (Math.max(absDx, absDy) >= axisLockThreshold) {
332
+ panStart.axisLock = absDy > absDx * axisLockRatio ? "y" : "free";
333
+ }
334
+ }
335
+ const rawTx = panStart.axisLock === "y" ? panStart.tx : panStart.tx + dx;
336
+ const rawTy = panStart.ty + dy;
337
+ const b = getBounds();
338
+ tx = rubberBandAxis(rawTx, b.minX, b.maxX, parent.clientWidth);
339
+ ty = rubberBandAxis(rawTy, b.minY, b.maxY, parent.clientHeight);
340
+ apply();
341
+ const now = performance.now();
342
+ const dt = Math.max(1, now - lastMoveTime);
343
+ const instVx = (e.clientX - lastMoveX) / dt;
344
+ const instVy = (e.clientY - lastMoveY) / dt;
345
+ const alpha = 0.85;
346
+ vx = vx * (1 - alpha) + instVx * alpha;
347
+ vy = vy * (1 - alpha) + instVy * alpha;
348
+ lastMoveTime = now;
349
+ lastMoveX = e.clientX;
350
+ lastMoveY = e.clientY;
351
+ }
352
+ }
353
+ function onUp(e) {
354
+ pointers.delete(e.pointerId);
355
+ if (pointers.size < 2) pinchStart = null;
356
+ if (pointers.size === 0) {
357
+ const hadPan = panStart !== null;
358
+ const lockedAxis = panStart?.axisLock;
359
+ panStart = null;
360
+ const idleMs = performance.now() - lastMoveTime;
361
+ if (idleMs > 60) {
362
+ vx = 0;
363
+ vy = 0;
364
+ }
365
+ if (hadPan && lockedAxis === "y") vx = 0;
366
+ startMomentum();
367
+ }
368
+ }
369
+ function onWheelZoom(e) {
370
+ if (!e.ctrlKey && !e.metaKey) return;
371
+ e.preventDefault();
372
+ stopMomentum();
373
+ const rect = parent.getBoundingClientRect();
374
+ const mx = e.clientX - rect.left;
375
+ const my = e.clientY - rect.top;
376
+ const delta = e.deltaY < 0 ? 1 : -1;
377
+ const newScale = Math.max(minScale, Math.min(maxScale, scale * Math.exp(delta * 0.1)));
378
+ const focalX = (mx - tx) / scale;
379
+ const focalY = (my - ty) / scale;
380
+ tx = mx - newScale * focalX;
381
+ ty = my - newScale * focalY;
382
+ scale = newScale;
383
+ apply();
384
+ }
385
+ function scrollBy(dx, dy) {
386
+ stopMomentum();
387
+ const b = getBounds();
388
+ tx = Math.max(b.minX, Math.min(b.maxX, tx - dx));
389
+ ty = Math.max(b.minY, Math.min(b.maxY, ty - dy));
390
+ apply();
391
+ }
392
+ parent.addEventListener("pointerdown", onDown);
393
+ parent.addEventListener("pointermove", onMove);
394
+ parent.addEventListener("pointerup", onUp);
395
+ parent.addEventListener("pointercancel", onUp);
396
+ parent.addEventListener("wheel", onWheelZoom, { passive: false });
397
+ target.addEventListener("pointerdown", onPenFilter);
398
+ apply();
399
+ controller = {
400
+ cleanup: () => {
401
+ stopMomentum();
402
+ clearTimeout(hideTimer);
403
+ parent.removeEventListener("pointerdown", onDown);
404
+ parent.removeEventListener("pointermove", onMove);
405
+ parent.removeEventListener("pointerup", onUp);
406
+ parent.removeEventListener("pointercancel", onUp);
407
+ parent.removeEventListener("wheel", onWheelZoom);
408
+ target.removeEventListener("pointerdown", onPenFilter);
409
+ vBar.remove();
410
+ hBar.remove();
411
+ target.style.transform = "";
412
+ target.style.transformOrigin = "";
413
+ target.style.willChange = "";
414
+ target.style.touchAction = "";
415
+ target.style.userSelect = "";
416
+ parent.style.touchAction = "";
417
+ },
418
+ reset: () => {
419
+ stopMomentum();
420
+ scale = 1;
421
+ tx = 0;
422
+ ty = 0;
423
+ vx = vy = 0;
424
+ pointers.clear();
425
+ pinchStart = null;
426
+ panStart = null;
427
+ setWillChangeActive(false);
428
+ apply();
429
+ },
430
+ stop: () => {
431
+ stopMomentum();
432
+ setWillChangeActive(false);
433
+ },
434
+ getScale: () => scale,
435
+ getTransform: () => ({ scale, tx, ty }),
436
+ scrollToElement,
437
+ scrollBy,
438
+ setGesturesEnabled: (enabled) => {
439
+ gesturesEnabled = enabled;
440
+ if (!enabled) {
441
+ pointers.clear();
442
+ panStart = null;
443
+ pinchStart = null;
444
+ stopMomentum();
445
+ setWillChangeActive(false);
446
+ }
447
+ },
448
+ onChange: (cb) => {
449
+ subscribers.add(cb);
450
+ return () => subscribers.delete(cb);
451
+ }
452
+ };
453
+ }
454
+ function detach() {
455
+ controller?.cleanup();
456
+ controller = null;
457
+ }
458
+ onBeforeUnmount(detach);
459
+ return {
460
+ attach,
461
+ detach,
462
+ reset: () => controller?.reset(),
463
+ stop: () => controller?.stop(),
464
+ getScale: () => controller?.getScale() ?? 1,
465
+ getTransform: () => controller?.getTransform() ?? { scale: 1, tx: 0, ty: 0 },
466
+ scrollToElement: (elem, opts) => controller?.scrollToElement(elem, opts),
467
+ scrollBy: (dx, dy) => controller?.scrollBy(dx, dy),
468
+ setGesturesEnabled: (enabled) => controller?.setGesturesEnabled(enabled),
469
+ onChange: (cb) => controller?.onChange(cb) ?? (() => {
470
+ })
471
+ };
472
+ }
@@ -13,6 +13,9 @@ export interface PageVirtualization {
13
13
  observePage: (pageNumber: number, element: HTMLElement) => void;
14
14
  unobservePage: (pageNumber: number) => void;
15
15
  scrollToPage: (pageNumber: number) => void;
16
+ getPageElement: (pageNumber: number) => HTMLElement | undefined;
17
+ /** Recompute currentPage using visual bounding rects. Works with CSS transforms. */
18
+ updateCurrentPageByRect: () => void;
16
19
  init: (doc: PDFDocumentProxy, scrollRoot: HTMLElement) => Promise<void>;
17
20
  destroy: () => void;
18
21
  }
@@ -96,6 +96,27 @@ export function createPageVirtualization() {
96
96
  currentPage.value = pageNumber;
97
97
  }
98
98
  }
99
+ function getPageElement(pageNumber) {
100
+ return pageToElement.get(pageNumber);
101
+ }
102
+ function updateCurrentPageByRect() {
103
+ if (!scrollRoot) return;
104
+ const rootRect = scrollRoot.getBoundingClientRect();
105
+ const rootMid = rootRect.top + rootRect.height / 2;
106
+ let bestPage = currentPage.value;
107
+ let bestDist = Infinity;
108
+ for (const [pageNum, el] of pageToElement) {
109
+ const rect = el.getBoundingClientRect();
110
+ const dist = rootMid < rect.top ? rect.top - rootMid : rootMid > rect.bottom ? rootMid - rect.bottom : 0;
111
+ if (dist < bestDist) {
112
+ bestDist = dist;
113
+ bestPage = pageNum;
114
+ }
115
+ }
116
+ if (bestPage !== currentPage.value) {
117
+ currentPage.value = bestPage;
118
+ }
119
+ }
99
120
  async function fetchPageDimensions(doc, pageNumber) {
100
121
  const proxy = await doc.getPage(pageNumber);
101
122
  const view = proxy.view;
@@ -197,6 +218,8 @@ export function createPageVirtualization() {
197
218
  observePage,
198
219
  unobservePage,
199
220
  scrollToPage,
221
+ getPageElement,
222
+ updateCurrentPageByRect,
200
223
  init,
201
224
  destroy
202
225
  };
@@ -27,6 +27,7 @@ export interface ViewerSearchState {
27
27
  totalPages: number;
28
28
  }) => void;
29
29
  setScrollToPage: (fn: (pageNumber: number) => void) => void;
30
+ setScrollToElement: (fn: (el: HTMLElement) => void) => void;
30
31
  }
31
32
  export declare function provideViewerSearch(): ViewerSearchState;
32
33
  export declare function useViewerSearch(): ViewerSearchState;
@@ -26,12 +26,16 @@ export function provideViewerSearch() {
26
26
  let inputElement = null;
27
27
  let _searchIndex = null;
28
28
  let _scrollToPage = null;
29
+ let _scrollToElement = null;
29
30
  function setSearchIndex(index) {
30
31
  _searchIndex = index;
31
32
  }
32
33
  function setScrollToPage(fn) {
33
34
  _scrollToPage = fn;
34
35
  }
36
+ function setScrollToElement(fn) {
37
+ _scrollToElement = fn;
38
+ }
35
39
  function getSortedPageNumbers() {
36
40
  return [...pageLayers.keys()].sort((a, b) => a - b);
37
41
  }
@@ -186,10 +190,14 @@ export function provideViewerSearch() {
186
190
  }
187
191
  prevEnd = end;
188
192
  if (isSelected && shouldScrollSelected && selectedSpan) {
189
- selectedSpan.scrollIntoView({
190
- block: "start",
191
- inline: "center"
192
- });
193
+ if (_scrollToElement) {
194
+ _scrollToElement(selectedSpan);
195
+ } else {
196
+ selectedSpan.scrollIntoView({
197
+ block: "start",
198
+ inline: "center"
199
+ });
200
+ }
193
201
  }
194
202
  }
195
203
  if (prevEnd) {
@@ -309,6 +317,7 @@ export function provideViewerSearch() {
309
317
  inputElement = null;
310
318
  _searchIndex = null;
311
319
  _scrollToPage = null;
320
+ _scrollToElement = null;
312
321
  }
313
322
  function setQuery(value) {
314
323
  query.value = value;
@@ -404,7 +413,8 @@ export function provideViewerSearch() {
404
413
  setInputElement,
405
414
  focusInput,
406
415
  setSearchIndex,
407
- setScrollToPage
416
+ setScrollToPage,
417
+ setScrollToElement
408
418
  };
409
419
  provide(VIEWER_SEARCH_KEY, state);
410
420
  return state;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kviewer",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Kabema PDF Editor",
5
5
  "repository": "kabema/kviewer",
6
6
  "license": "MIT",