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.
- package/dist/module.json +1 -1
- package/dist/runtime/annotation/engine/editor/editor.d.ts +6 -3
- package/dist/runtime/annotation/engine/editor/editor.js +22 -9
- package/dist/runtime/annotation/engine/editor/selector.d.ts +9 -1
- package/dist/runtime/annotation/engine/editor/selector.js +72 -30
- package/dist/runtime/annotation/engine/painter.d.ts +2 -1
- package/dist/runtime/annotation/engine/painter.js +16 -4
- package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
- package/dist/runtime/components/PdfPage.vue +51 -18
- package/dist/runtime/components/PdfPage.vue.d.ts +6 -1
- package/dist/runtime/components/ToolButton.d.vue.ts +4 -2
- package/dist/runtime/components/ToolButton.vue.d.ts +4 -2
- package/dist/runtime/components/Viewer.vue +213 -164
- package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
- package/dist/runtime/components/tools/ActionTools.vue +4 -5
- package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
- package/dist/runtime/composables/useAnnotationEngine.js +4 -1
- package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
- package/dist/runtime/composables/useAnnotationHistory.js +10 -3
- package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
- package/dist/runtime/composables/useInertiaPanzoom.js +472 -0
- package/dist/runtime/composables/usePageVirtualization.d.ts +3 -0
- package/dist/runtime/composables/usePageVirtualization.js +23 -0
- package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
- package/dist/runtime/composables/useViewerSearch.js +15 -5
- package/package.json +1 -1
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<div class="relative flex-1 min-h-0">
|
|
9
9
|
<div
|
|
10
10
|
ref="scrollContainer"
|
|
11
|
-
class="absolute inset-0 overflow-
|
|
12
|
-
|
|
11
|
+
class="absolute inset-0 overflow-hidden bg-[#F0F3F5] dark:bg-muted/50"
|
|
12
|
+
style="overscroll-behavior: none"
|
|
13
13
|
data-testid="viewer-scroll"
|
|
14
14
|
>
|
|
15
15
|
<!-- Error state -->
|
|
@@ -39,24 +39,29 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
|
|
41
41
|
<ClientOnly v-else>
|
|
42
|
-
<!-- Single-page-
|
|
42
|
+
<!-- Single-page mode with panzoom for pinch-to-zoom -->
|
|
43
43
|
<div
|
|
44
44
|
v-if="isSinglePageMode && singlePageMeta"
|
|
45
|
-
class="
|
|
46
|
-
:style="{ minHeight: '100%' }"
|
|
45
|
+
class="panzoom-viewport"
|
|
47
46
|
>
|
|
48
47
|
<div
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
ref="zoomWrapper"
|
|
49
|
+
class="flex flex-col items-center justify-center p-4"
|
|
50
|
+
:style="{ minHeight: '100%' }"
|
|
52
51
|
>
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
<div
|
|
53
|
+
:ref="(el) => onPageRef(singlePageMeta.pageNumber, el)"
|
|
54
|
+
:style="pageSlotStyle(singlePageMeta)"
|
|
55
|
+
class="kviewer-page-slot"
|
|
56
|
+
>
|
|
57
|
+
<div :style="pageRotationStyle(singlePageMeta)">
|
|
58
|
+
<PdfPage
|
|
59
|
+
:key="singlePageMeta.pageNumber"
|
|
60
|
+
:page-number="singlePageMeta.pageNumber"
|
|
61
|
+
:page-meta="singlePageMeta"
|
|
62
|
+
:scale="pageScale"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
60
65
|
</div>
|
|
61
66
|
</div>
|
|
62
67
|
</div>
|
|
@@ -64,38 +69,14 @@
|
|
|
64
69
|
<!-- Continuous scroll — single-page layout -->
|
|
65
70
|
<div
|
|
66
71
|
v-else-if="pageSettings.layoutMode.value === 'single'"
|
|
67
|
-
class="
|
|
72
|
+
class="panzoom-viewport"
|
|
68
73
|
>
|
|
69
74
|
<div
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
:ref="(el) => onPageRef(meta.pageNumber, el)"
|
|
73
|
-
:style="pageSlotStyle(meta)"
|
|
74
|
-
class="kviewer-page-slot"
|
|
75
|
-
>
|
|
76
|
-
<div :style="pageRotationStyle(meta)">
|
|
77
|
-
<PdfPage
|
|
78
|
-
v-if="isPageRendered(meta.pageNumber)"
|
|
79
|
-
:page-number="meta.pageNumber"
|
|
80
|
-
:page-meta="meta"
|
|
81
|
-
:scale="pageScale"
|
|
82
|
-
/>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<!-- Continuous scroll — two-page (side-by-side) layout -->
|
|
88
|
-
<div
|
|
89
|
-
v-else
|
|
90
|
-
class="flex flex-col items-center gap-4 py-4"
|
|
91
|
-
>
|
|
92
|
-
<div
|
|
93
|
-
v-for="(pair, index) in pagedPairs"
|
|
94
|
-
:key="index"
|
|
95
|
-
class="flex gap-4"
|
|
75
|
+
ref="zoomWrapper"
|
|
76
|
+
class="flex flex-col gap-4 p-4"
|
|
96
77
|
>
|
|
97
78
|
<div
|
|
98
|
-
v-for="meta in
|
|
79
|
+
v-for="meta in pageMetas"
|
|
99
80
|
:key="meta.pageNumber"
|
|
100
81
|
:ref="(el) => onPageRef(meta.pageNumber, el)"
|
|
101
82
|
:style="pageSlotStyle(meta)"
|
|
@@ -112,6 +93,40 @@
|
|
|
112
93
|
</div>
|
|
113
94
|
</div>
|
|
114
95
|
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Continuous scroll — two-page (side-by-side) layout -->
|
|
98
|
+
<div
|
|
99
|
+
v-else
|
|
100
|
+
class="panzoom-viewport"
|
|
101
|
+
>
|
|
102
|
+
<div
|
|
103
|
+
ref="zoomWrapper"
|
|
104
|
+
class="flex flex-col gap-4 p-4"
|
|
105
|
+
>
|
|
106
|
+
<div
|
|
107
|
+
v-for="(pair, index) in pagedPairs"
|
|
108
|
+
:key="index"
|
|
109
|
+
class="flex gap-4"
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
v-for="meta in pair"
|
|
113
|
+
:key="meta.pageNumber"
|
|
114
|
+
:ref="(el) => onPageRef(meta.pageNumber, el)"
|
|
115
|
+
:style="pageSlotStyle(meta)"
|
|
116
|
+
class="kviewer-page-slot"
|
|
117
|
+
>
|
|
118
|
+
<div :style="pageRotationStyle(meta)">
|
|
119
|
+
<PdfPage
|
|
120
|
+
v-if="isPageRendered(meta.pageNumber)"
|
|
121
|
+
:page-number="meta.pageNumber"
|
|
122
|
+
:page-meta="meta"
|
|
123
|
+
:scale="pageScale"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
115
130
|
</ClientOnly>
|
|
116
131
|
</div>
|
|
117
132
|
|
|
@@ -136,6 +151,16 @@
|
|
|
136
151
|
@submit="onFreeTextSubmit"
|
|
137
152
|
@cancel="onFreeTextCancel"
|
|
138
153
|
/>
|
|
154
|
+
<!-- iOS focus bridge: focused synchronously in the tap handler so the
|
|
155
|
+
keyboard pops up; modal textarea takes over on mount without
|
|
156
|
+
closing the keyboard. -->
|
|
157
|
+
<input
|
|
158
|
+
ref="iosFocusBridge"
|
|
159
|
+
type="text"
|
|
160
|
+
aria-hidden="true"
|
|
161
|
+
tabindex="-1"
|
|
162
|
+
class="ios-focus-bridge"
|
|
163
|
+
>
|
|
139
164
|
</ClientOnly>
|
|
140
165
|
</div>
|
|
141
166
|
</template>
|
|
@@ -174,7 +199,8 @@ import { createPageProxyCache } from "../composables/usePageProxyCache";
|
|
|
174
199
|
import { createSearchIndex } from "../composables/useSearchIndex";
|
|
175
200
|
import { createShapeDetection } from "../composables/useShapeDetection";
|
|
176
201
|
import { provideFormFields } from "../composables/useFormFields";
|
|
177
|
-
import { isIPad
|
|
202
|
+
import { isIPad } from "../annotation/engine/input-device";
|
|
203
|
+
import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
|
|
178
204
|
const props = defineProps({
|
|
179
205
|
source: { type: null, required: true },
|
|
180
206
|
textLayer: { type: Boolean, required: false },
|
|
@@ -204,8 +230,12 @@ const virtualization = createPageVirtualization();
|
|
|
204
230
|
setScrollToPageFn((pageNumber) => {
|
|
205
231
|
if (isSinglePageMode.value) {
|
|
206
232
|
viewerState.currentPage.value = pageNumber;
|
|
207
|
-
|
|
208
|
-
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const el = virtualization.getPageElement(pageNumber);
|
|
236
|
+
if (el) {
|
|
237
|
+
inertiaPanzoom.scrollToElement(el, { block: "start" });
|
|
238
|
+
viewerState.currentPage.value = pageNumber;
|
|
209
239
|
}
|
|
210
240
|
});
|
|
211
241
|
const proxyCache = createPageProxyCache();
|
|
@@ -241,6 +271,8 @@ const isRotatedSideways = computed(() => {
|
|
|
241
271
|
const r = pageSettings.rotation.value;
|
|
242
272
|
return r === 90 || r === 270;
|
|
243
273
|
});
|
|
274
|
+
const zoomWrapper = ref(null);
|
|
275
|
+
const inertiaPanzoom = useInertiaPanzoom({ minScale: 0.5, maxScale: 4 });
|
|
244
276
|
function pageSlotStyle(meta) {
|
|
245
277
|
const s = pageScale.value;
|
|
246
278
|
if (isRotatedSideways.value) {
|
|
@@ -297,8 +329,10 @@ const pagedPairs = computed(() => {
|
|
|
297
329
|
});
|
|
298
330
|
const freeTextModalOpen = ref(false);
|
|
299
331
|
const freeTextDefaults = ref({ color: "#ff0000", fontSize: 18 });
|
|
332
|
+
const iosFocusBridge = ref(null);
|
|
300
333
|
let freeTextResolve = null;
|
|
301
334
|
function onRequestTextInput(opts) {
|
|
335
|
+
iosFocusBridge.value?.focus({ preventScroll: true });
|
|
302
336
|
return new Promise((resolve) => {
|
|
303
337
|
freeTextDefaults.value = {
|
|
304
338
|
color: opts.defaultColor,
|
|
@@ -342,6 +376,12 @@ async function loadDocument() {
|
|
|
342
376
|
viewerState.doc.value = pdfDoc;
|
|
343
377
|
viewerState.totalPages.value = pdfDoc.numPages;
|
|
344
378
|
proxyCache.setDocument(pdfDoc);
|
|
379
|
+
if (!scrollContainer.value) {
|
|
380
|
+
await nextTick();
|
|
381
|
+
}
|
|
382
|
+
if (!scrollContainer.value) {
|
|
383
|
+
throw new Error("Scroll container not available");
|
|
384
|
+
}
|
|
345
385
|
await virtualization.init(pdfDoc, scrollContainer.value);
|
|
346
386
|
const metas = virtualization.pageMetas.value;
|
|
347
387
|
if (metas.length > 0) {
|
|
@@ -378,7 +418,17 @@ async function loadDocument() {
|
|
|
378
418
|
);
|
|
379
419
|
}
|
|
380
420
|
viewerSearch.setScrollToPage((pageNumber) => {
|
|
381
|
-
|
|
421
|
+
if (isSinglePageMode.value) {
|
|
422
|
+
viewerState.currentPage.value = pageNumber;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const el = virtualization.getPageElement(pageNumber);
|
|
426
|
+
if (el) inertiaPanzoom.scrollToElement(el, { block: "start" });
|
|
427
|
+
});
|
|
428
|
+
viewerSearch.setScrollToElement((el) => {
|
|
429
|
+
if (!isSinglePageMode.value) {
|
|
430
|
+
inertiaPanzoom.scrollToElement(el, { block: "center" });
|
|
431
|
+
}
|
|
382
432
|
});
|
|
383
433
|
searchIndex.buildIndex(pdfDoc).then(() => {
|
|
384
434
|
viewerSearch.setSearchIndex({
|
|
@@ -481,116 +531,97 @@ async function importAnnotations(annotations, options) {
|
|
|
481
531
|
function getKonvaCanvasState() {
|
|
482
532
|
return painter?.getCanvasState() ?? {};
|
|
483
533
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
let pinchOriginX = 0;
|
|
489
|
-
let pinchOriginY = 0;
|
|
490
|
-
let pinchStartViewportX = 0;
|
|
491
|
-
let pinchStartViewportY = 0;
|
|
492
|
-
let pinchCurrentViewportX = 0;
|
|
493
|
-
let pinchCurrentViewportY = 0;
|
|
494
|
-
function getPinchTarget() {
|
|
495
|
-
return scrollContainer.value?.firstElementChild;
|
|
496
|
-
}
|
|
497
|
-
function onPinchTouchStart(e) {
|
|
498
|
-
if (!viewerState.stylusMode.value) return;
|
|
499
|
-
if (e.touches.length === 2) {
|
|
500
|
-
const sc = scrollContainer.value;
|
|
501
|
-
const scRect = sc.getBoundingClientRect();
|
|
502
|
-
const center = getTouchCenter(e.touches[0], e.touches[1]);
|
|
503
|
-
pinchActive = true;
|
|
504
|
-
pinchStartDistance = getTouchDistance(e.touches[0], e.touches[1]);
|
|
505
|
-
pinchStartScale = viewerState.scale.value;
|
|
506
|
-
pinchCurrentRatio = 1;
|
|
507
|
-
pinchStartViewportX = center.x - scRect.left;
|
|
508
|
-
pinchStartViewportY = center.y - scRect.top;
|
|
509
|
-
pinchCurrentViewportX = pinchStartViewportX;
|
|
510
|
-
pinchCurrentViewportY = pinchStartViewportY;
|
|
511
|
-
pinchOriginX = pinchStartViewportX + sc.scrollLeft;
|
|
512
|
-
pinchOriginY = pinchStartViewportY + sc.scrollTop;
|
|
513
|
-
const target = getPinchTarget();
|
|
514
|
-
if (target) {
|
|
515
|
-
target.style.transformOrigin = `${pinchOriginX}px ${pinchOriginY}px`;
|
|
516
|
-
target.style.willChange = "transform";
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
function onPinchTouchMove(e) {
|
|
521
|
-
if (!viewerState.stylusMode.value || !pinchActive || e.touches.length < 2) return;
|
|
522
|
-
e.preventDefault();
|
|
523
|
-
const sc = scrollContainer.value;
|
|
524
|
-
if (!sc) return;
|
|
525
|
-
const scRect = sc.getBoundingClientRect();
|
|
526
|
-
const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
|
|
527
|
-
pinchCurrentRatio = currentDistance / pinchStartDistance;
|
|
528
|
-
const clamped = clampScale(pinchStartScale * pinchCurrentRatio);
|
|
529
|
-
pinchCurrentRatio = clamped / pinchStartScale;
|
|
530
|
-
const center = getTouchCenter(e.touches[0], e.touches[1]);
|
|
531
|
-
pinchCurrentViewportX = center.x - scRect.left;
|
|
532
|
-
pinchCurrentViewportY = center.y - scRect.top;
|
|
533
|
-
const target = getPinchTarget();
|
|
534
|
-
if (target) target.style.transform = `scale(${pinchCurrentRatio})`;
|
|
535
|
-
sc.scrollLeft = pinchOriginX * pinchCurrentRatio - pinchCurrentViewportX;
|
|
536
|
-
sc.scrollTop = pinchOriginY * pinchCurrentRatio - pinchCurrentViewportY;
|
|
537
|
-
}
|
|
538
|
-
function onPinchTouchEnd(e) {
|
|
539
|
-
if (e.touches.length < 2 && pinchActive) {
|
|
540
|
-
pinchActive = false;
|
|
541
|
-
const target = getPinchTarget();
|
|
542
|
-
if (target) {
|
|
543
|
-
target.style.transform = "";
|
|
544
|
-
target.style.willChange = "";
|
|
545
|
-
}
|
|
546
|
-
const finalScale = clampScale(pinchStartScale * pinchCurrentRatio);
|
|
547
|
-
if (Math.abs(finalScale - pinchStartScale) > 0.01) {
|
|
548
|
-
const layoutRatio = finalScale / pinchStartScale;
|
|
549
|
-
const newScrollLeft = pinchOriginX * layoutRatio - pinchCurrentViewportX;
|
|
550
|
-
const newScrollTop = pinchOriginY * layoutRatio - pinchCurrentViewportY;
|
|
551
|
-
viewerState.setScale(finalScale);
|
|
552
|
-
nextTick(() => {
|
|
553
|
-
const sc = scrollContainer.value;
|
|
554
|
-
if (sc) {
|
|
555
|
-
sc.scrollLeft = newScrollLeft;
|
|
556
|
-
sc.scrollTop = newScrollTop;
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
pinchCurrentRatio = 1;
|
|
561
|
-
}
|
|
534
|
+
function initPanzoom() {
|
|
535
|
+
inertiaPanzoom.detach();
|
|
536
|
+
if (!zoomWrapper.value) return;
|
|
537
|
+
inertiaPanzoom.attach(zoomWrapper.value);
|
|
562
538
|
}
|
|
563
539
|
let wheelCooldown = false;
|
|
564
|
-
function
|
|
565
|
-
if (
|
|
566
|
-
event.preventDefault();
|
|
567
|
-
if (wheelCooldown) return;
|
|
568
|
-
wheelCooldown = true;
|
|
569
|
-
setTimeout(() => {
|
|
570
|
-
wheelCooldown = false;
|
|
571
|
-
}, 300);
|
|
572
|
-
const total = viewerState.totalPages.value;
|
|
573
|
-
const cur = viewerState.currentPage.value;
|
|
574
|
-
if (event.deltaY > 0 && cur < total) {
|
|
575
|
-
viewerState.currentPage.value = cur + 1;
|
|
576
|
-
} else if (event.deltaY < 0 && cur > 1) {
|
|
577
|
-
viewerState.currentPage.value = cur - 1;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
function onGlobalKeydown(event) {
|
|
581
|
-
if (!props.active) return;
|
|
540
|
+
function onWheel(event) {
|
|
541
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
582
542
|
if (isSinglePageMode.value) {
|
|
543
|
+
event.preventDefault();
|
|
544
|
+
if (wheelCooldown) return;
|
|
545
|
+
wheelCooldown = true;
|
|
546
|
+
setTimeout(() => {
|
|
547
|
+
wheelCooldown = false;
|
|
548
|
+
}, 300);
|
|
583
549
|
const total = viewerState.totalPages.value;
|
|
584
550
|
const cur = viewerState.currentPage.value;
|
|
585
|
-
if (
|
|
586
|
-
event.preventDefault();
|
|
551
|
+
if (event.deltaY > 0 && cur < total) {
|
|
587
552
|
viewerState.currentPage.value = cur + 1;
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
if ((event.key === "ArrowUp" || event.key === "PageUp") && cur > 1) {
|
|
591
|
-
event.preventDefault();
|
|
553
|
+
} else if (event.deltaY < 0 && cur > 1) {
|
|
592
554
|
viewerState.currentPage.value = cur - 1;
|
|
593
|
-
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
event.preventDefault();
|
|
559
|
+
const dx = event.shiftKey ? event.deltaY : event.deltaX;
|
|
560
|
+
const dy = event.shiftKey ? 0 : event.deltaY;
|
|
561
|
+
inertiaPanzoom.scrollBy(dx, dy);
|
|
562
|
+
}
|
|
563
|
+
function isTextInputTarget(target) {
|
|
564
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
565
|
+
const tag = target.tagName;
|
|
566
|
+
return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
|
|
567
|
+
}
|
|
568
|
+
function onGlobalKeydown(event) {
|
|
569
|
+
if (!props.active) return;
|
|
570
|
+
const typing = isTextInputTarget(event.target);
|
|
571
|
+
if (!typing) {
|
|
572
|
+
if (isSinglePageMode.value) {
|
|
573
|
+
const total = viewerState.totalPages.value;
|
|
574
|
+
const cur = viewerState.currentPage.value;
|
|
575
|
+
if ((event.key === "ArrowDown" || event.key === "PageDown") && cur < total) {
|
|
576
|
+
event.preventDefault();
|
|
577
|
+
viewerState.currentPage.value = cur + 1;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if ((event.key === "ArrowUp" || event.key === "PageUp") && cur > 1) {
|
|
581
|
+
event.preventDefault();
|
|
582
|
+
viewerState.currentPage.value = cur - 1;
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
const vh = scrollContainer.value?.clientHeight ?? 600;
|
|
587
|
+
const arrowStep = 40;
|
|
588
|
+
const pageStep = Math.max(100, vh * 0.9);
|
|
589
|
+
const BIG = 1e9;
|
|
590
|
+
switch (event.key) {
|
|
591
|
+
case "ArrowDown":
|
|
592
|
+
event.preventDefault();
|
|
593
|
+
inertiaPanzoom.scrollBy(0, arrowStep);
|
|
594
|
+
return;
|
|
595
|
+
case "ArrowUp":
|
|
596
|
+
event.preventDefault();
|
|
597
|
+
inertiaPanzoom.scrollBy(0, -arrowStep);
|
|
598
|
+
return;
|
|
599
|
+
case "ArrowRight":
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
inertiaPanzoom.scrollBy(arrowStep, 0);
|
|
602
|
+
return;
|
|
603
|
+
case "ArrowLeft":
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
inertiaPanzoom.scrollBy(-arrowStep, 0);
|
|
606
|
+
return;
|
|
607
|
+
case "PageDown":
|
|
608
|
+
case " ":
|
|
609
|
+
event.preventDefault();
|
|
610
|
+
inertiaPanzoom.scrollBy(0, pageStep);
|
|
611
|
+
return;
|
|
612
|
+
case "PageUp":
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
inertiaPanzoom.scrollBy(0, -pageStep);
|
|
615
|
+
return;
|
|
616
|
+
case "Home":
|
|
617
|
+
event.preventDefault();
|
|
618
|
+
inertiaPanzoom.scrollBy(0, -BIG);
|
|
619
|
+
return;
|
|
620
|
+
case "End":
|
|
621
|
+
event.preventDefault();
|
|
622
|
+
inertiaPanzoom.scrollBy(0, BIG);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
594
625
|
}
|
|
595
626
|
}
|
|
596
627
|
const isFindShortcut = (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "f";
|
|
@@ -611,7 +642,12 @@ onMounted(() => {
|
|
|
611
642
|
painter = createAnnotationEngine(viewerState, {
|
|
612
643
|
userName: props.userName,
|
|
613
644
|
onRequestTextInput,
|
|
614
|
-
freehandGroupingDelay: props.freehandGroupingDelay
|
|
645
|
+
freehandGroupingDelay: props.freehandGroupingDelay,
|
|
646
|
+
// Suspend panzoom gestures while an annotation is being dragged or
|
|
647
|
+
// transformed. Otherwise single-finger pan runs in parallel with the
|
|
648
|
+
// Konva shape drag on mobile and the two CSS transforms fight every
|
|
649
|
+
// frame, making text drag feel laggy.
|
|
650
|
+
setExternalGesturesEnabled: (enabled) => inertiaPanzoom.setGesturesEnabled(enabled)
|
|
615
651
|
});
|
|
616
652
|
viewerState.selectTool(viewerState.activeTool.value);
|
|
617
653
|
if (props.stamps) viewerState.stamps.value = props.stamps;
|
|
@@ -630,14 +666,26 @@ onMounted(() => {
|
|
|
630
666
|
}
|
|
631
667
|
});
|
|
632
668
|
resizeObserver.observe(scrollContainer.value);
|
|
633
|
-
scrollContainer.value.addEventListener("
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
669
|
+
scrollContainer.value.addEventListener("wheel", onWheel, { passive: false });
|
|
670
|
+
inertiaPanzoom.onChange(() => {
|
|
671
|
+
if (viewerState.selectionRect.value) viewerState.selectionRect.value = null;
|
|
672
|
+
if (!isSinglePageMode.value) virtualization.updateCurrentPageByRect();
|
|
673
|
+
});
|
|
674
|
+
nextTick(() => initPanzoom());
|
|
675
|
+
watch(
|
|
676
|
+
[isSinglePageMode, () => pageSettings.layoutMode.value],
|
|
677
|
+
() => nextTick(() => initPanzoom())
|
|
678
|
+
);
|
|
679
|
+
watch(
|
|
680
|
+
[() => viewerState.activeTool.value, () => viewerState.stylusMode.value],
|
|
681
|
+
([tool, stylusMode]) => {
|
|
682
|
+
const enable = stylusMode || tool === "hand";
|
|
683
|
+
inertiaPanzoom.setGesturesEnabled(enable);
|
|
684
|
+
},
|
|
685
|
+
{ immediate: true }
|
|
686
|
+
);
|
|
687
|
+
watch(() => viewerState.scale.value, () => inertiaPanzoom.reset());
|
|
688
|
+
watch(() => viewerState.currentPage.value, () => inertiaPanzoom.reset());
|
|
641
689
|
}
|
|
642
690
|
if (isIPad()) {
|
|
643
691
|
viewerState.setStylusMode(true);
|
|
@@ -687,11 +735,8 @@ watch(
|
|
|
687
735
|
);
|
|
688
736
|
onBeforeUnmount(() => {
|
|
689
737
|
window.removeEventListener("keydown", onGlobalKeydown);
|
|
690
|
-
scrollContainer.value?.removeEventListener("wheel",
|
|
691
|
-
|
|
692
|
-
scrollContainer.value?.removeEventListener("touchmove", onPinchTouchMove);
|
|
693
|
-
scrollContainer.value?.removeEventListener("touchend", onPinchTouchEnd);
|
|
694
|
-
scrollContainer.value?.removeEventListener("touchcancel", onPinchTouchEnd);
|
|
738
|
+
scrollContainer.value?.removeEventListener("wheel", onWheel);
|
|
739
|
+
inertiaPanzoom.detach();
|
|
695
740
|
resizeObserver?.disconnect();
|
|
696
741
|
viewerSearch.reset();
|
|
697
742
|
shapeDetectionCache.clearAllShapeCache();
|
|
@@ -710,3 +755,7 @@ defineExpose({
|
|
|
710
755
|
setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value)
|
|
711
756
|
});
|
|
712
757
|
</script>
|
|
758
|
+
|
|
759
|
+
<style scoped>
|
|
760
|
+
.kviewer-page-slot{margin-left:auto;margin-right:auto}.panzoom-viewport{height:100%;overflow:hidden;position:relative;width:100%}.ios-focus-bridge{border:0;font-size:16px;height:1px;left:0;opacity:0;padding:0;pointer-events:none;position:fixed;top:0;width:1px;z-index:-1}
|
|
761
|
+
</style>
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<template #body>
|
|
8
8
|
<div class="flex flex-col gap-3">
|
|
9
9
|
<UTextarea
|
|
10
|
+
ref="textareaRef"
|
|
10
11
|
v-model="inputValue"
|
|
11
12
|
placeholder="Start typing..."
|
|
12
13
|
:rows="3"
|
|
13
|
-
autofocus
|
|
14
14
|
/>
|
|
15
15
|
<div class="flex items-center justify-between">
|
|
16
16
|
<div class="flex gap-1">
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
c === currentColor ? 'border-white ring-2 ring-blue-500' : 'border-transparent'
|
|
23
23
|
"
|
|
24
24
|
:style="{ backgroundColor: c }"
|
|
25
|
+
@pointerdown="onPreserveFocus"
|
|
25
26
|
@click="currentColor = c"
|
|
26
27
|
/>
|
|
27
28
|
</div>
|
|
@@ -39,14 +40,19 @@
|
|
|
39
40
|
<UButton variant="ghost" color="neutral" @click="cancel">
|
|
40
41
|
Cancel
|
|
41
42
|
</UButton>
|
|
42
|
-
<UButton
|
|
43
|
+
<UButton
|
|
44
|
+
:disabled="!inputValue.trim()"
|
|
45
|
+
@click="submit"
|
|
46
|
+
>
|
|
47
|
+
OK
|
|
48
|
+
</UButton>
|
|
43
49
|
</div>
|
|
44
50
|
</template>
|
|
45
51
|
</UModal>
|
|
46
52
|
</template>
|
|
47
53
|
|
|
48
54
|
<script setup>
|
|
49
|
-
import { ref, watch } from "vue";
|
|
55
|
+
import { ref, watch, nextTick } from "vue";
|
|
50
56
|
import { defaultOptions } from "../../annotation/engine/config";
|
|
51
57
|
const props = defineProps({
|
|
52
58
|
open: { type: Boolean, required: true },
|
|
@@ -55,6 +61,20 @@ const props = defineProps({
|
|
|
55
61
|
initialText: { type: String, required: false }
|
|
56
62
|
});
|
|
57
63
|
const emit = defineEmits(["update:open", "submit", "cancel"]);
|
|
64
|
+
const textareaRef = ref(null);
|
|
65
|
+
function focusTextarea() {
|
|
66
|
+
const instance = textareaRef.value;
|
|
67
|
+
if (!instance) return;
|
|
68
|
+
const el = instance.inputRef ?? instance.$el?.querySelector?.("textarea") ?? instance;
|
|
69
|
+
if (el && typeof el.focus === "function") {
|
|
70
|
+
el.focus();
|
|
71
|
+
const len = el.value?.length ?? 0;
|
|
72
|
+
try {
|
|
73
|
+
el.setSelectionRange(len, len);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
58
78
|
const isOpen = ref(props.open);
|
|
59
79
|
watch(
|
|
60
80
|
() => props.open,
|
|
@@ -64,6 +84,7 @@ watch(
|
|
|
64
84
|
inputValue.value = props.initialText ?? "";
|
|
65
85
|
currentColor.value = props.defaultColor;
|
|
66
86
|
currentFontSize.value = String(props.defaultFontSize);
|
|
87
|
+
nextTick(() => requestAnimationFrame(() => focusTextarea()));
|
|
67
88
|
}
|
|
68
89
|
}
|
|
69
90
|
);
|
|
@@ -86,4 +107,7 @@ function submit() {
|
|
|
86
107
|
function cancel() {
|
|
87
108
|
emit("cancel");
|
|
88
109
|
}
|
|
110
|
+
function onPreserveFocus(e) {
|
|
111
|
+
if (e.pointerType === "mouse") e.preventDefault();
|
|
112
|
+
}
|
|
89
113
|
</script>
|
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
:disabled="!state.history.canRedo.value"
|
|
19
19
|
@click="state.history.redo"
|
|
20
20
|
/>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
:
|
|
24
|
-
color="neutral"
|
|
25
|
-
size="sm"
|
|
21
|
+
<ToolButton
|
|
22
|
+
:tool="{ name: 'Eraser', icon: 'i-lucide-eraser' }"
|
|
23
|
+
:active="state.activeTool.value === 'eraser'"
|
|
26
24
|
data-testid="action-eraser"
|
|
27
25
|
@click="state.selectTool('eraser')"
|
|
28
26
|
/>
|
|
@@ -31,5 +29,6 @@
|
|
|
31
29
|
|
|
32
30
|
<script setup>
|
|
33
31
|
import { useViewerState } from "../../composables/useViewerState";
|
|
32
|
+
import ToolButton from "../ToolButton.vue";
|
|
34
33
|
const state = useViewerState();
|
|
35
34
|
</script>
|
|
@@ -5,4 +5,5 @@ export declare function createAnnotationEngine(viewerState: ViewerState, options
|
|
|
5
5
|
onRequestTextInput: PainterCallbacks['onRequestTextInput'];
|
|
6
6
|
onRequestDeleteConfirm?: PainterCallbacks['onRequestDeleteConfirm'];
|
|
7
7
|
freehandGroupingDelay?: number;
|
|
8
|
+
setExternalGesturesEnabled?: (enabled: boolean) => void;
|
|
8
9
|
}): Painter;
|
|
@@ -21,6 +21,8 @@ export function createAnnotationEngine(viewerState, options) {
|
|
|
21
21
|
if (annotationStore.type === AnnotationType.FREETEXT) {
|
|
22
22
|
viewerState.painter.value?.selectAnnotation(annotationStore.id);
|
|
23
23
|
}
|
|
24
|
+
} else {
|
|
25
|
+
viewerState.history.rebaseline();
|
|
24
26
|
}
|
|
25
27
|
},
|
|
26
28
|
onStoreDelete: (id) => {
|
|
@@ -65,7 +67,8 @@ export function createAnnotationEngine(viewerState, options) {
|
|
|
65
67
|
callbacks,
|
|
66
68
|
getStylusModeEnabled: () => viewerState.stylusMode.value,
|
|
67
69
|
getReadonly: () => viewerState.readonly.value,
|
|
68
|
-
freehandGroupingDelay: options.freehandGroupingDelay
|
|
70
|
+
freehandGroupingDelay: options.freehandGroupingDelay,
|
|
71
|
+
setExternalGesturesEnabled: options.setExternalGesturesEnabled
|
|
69
72
|
});
|
|
70
73
|
viewerState.painter.value = painter;
|
|
71
74
|
return painter;
|
|
@@ -6,15 +6,19 @@ function cloneAnnotations(map) {
|
|
|
6
6
|
export function createAnnotationHistory(annotations, painter) {
|
|
7
7
|
const undoStack = ref([]);
|
|
8
8
|
const redoStack = ref([]);
|
|
9
|
+
let baseline = cloneAnnotations(annotations.value);
|
|
9
10
|
const canUndo = computed(() => undoStack.value.length > 0);
|
|
10
11
|
const canRedo = computed(() => redoStack.value.length > 0);
|
|
11
12
|
function commit() {
|
|
12
|
-
|
|
13
|
-
undoStack.value.push(snapshot);
|
|
13
|
+
undoStack.value.push(baseline);
|
|
14
14
|
if (undoStack.value.length > MAX_HISTORY) {
|
|
15
15
|
undoStack.value.shift();
|
|
16
16
|
}
|
|
17
17
|
redoStack.value = [];
|
|
18
|
+
baseline = cloneAnnotations(annotations.value);
|
|
19
|
+
}
|
|
20
|
+
function rebaseline() {
|
|
21
|
+
baseline = cloneAnnotations(annotations.value);
|
|
18
22
|
}
|
|
19
23
|
function restoreSnapshot(target) {
|
|
20
24
|
const p = painter.value;
|
|
@@ -53,6 +57,7 @@ export function createAnnotationHistory(annotations, painter) {
|
|
|
53
57
|
redoStack.value.push(currentSnapshot);
|
|
54
58
|
const target = undoStack.value.pop();
|
|
55
59
|
restoreSnapshot(target);
|
|
60
|
+
baseline = cloneAnnotations(annotations.value);
|
|
56
61
|
}
|
|
57
62
|
function redo() {
|
|
58
63
|
if (!canRedo.value) return;
|
|
@@ -60,10 +65,12 @@ export function createAnnotationHistory(annotations, painter) {
|
|
|
60
65
|
undoStack.value.push(currentSnapshot);
|
|
61
66
|
const target = redoStack.value.pop();
|
|
62
67
|
restoreSnapshot(target);
|
|
68
|
+
baseline = cloneAnnotations(annotations.value);
|
|
63
69
|
}
|
|
64
70
|
function reset() {
|
|
65
71
|
undoStack.value = [];
|
|
66
72
|
redoStack.value = [];
|
|
73
|
+
baseline = cloneAnnotations(annotations.value);
|
|
67
74
|
}
|
|
68
|
-
return { commit, undo, redo, reset, canUndo, canRedo };
|
|
75
|
+
return { commit, undo, redo, reset, rebaseline, canUndo, canRedo };
|
|
69
76
|
}
|