kviewer 0.2.1 → 0.2.3

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 (111) hide show
  1. package/README.md +19 -0
  2. package/dist/module.d.mts +8 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +25 -3
  5. package/dist/runtime/annotation/engine/canvas-resolution.d.ts +7 -0
  6. package/dist/runtime/annotation/engine/canvas-resolution.js +21 -0
  7. package/dist/runtime/annotation/engine/editor/editor.d.ts +7 -2
  8. package/dist/runtime/annotation/engine/editor/editor.js +14 -0
  9. package/dist/runtime/annotation/engine/editor/selector.d.ts +13 -1
  10. package/dist/runtime/annotation/engine/editor/selector.js +60 -8
  11. package/dist/runtime/annotation/engine/import-normalize.js +3 -3
  12. package/dist/runtime/annotation/engine/painter.d.ts +33 -2
  13. package/dist/runtime/annotation/engine/painter.js +88 -2
  14. package/dist/runtime/annotation/engine/store.js +3 -0
  15. package/dist/runtime/annotation/engine/tools/arrow.js +0 -1
  16. package/dist/runtime/annotation/engine/tools/circle.js +0 -2
  17. package/dist/runtime/annotation/engine/tools/free-highlight.js +0 -2
  18. package/dist/runtime/annotation/engine/tools/freehand.js +0 -2
  19. package/dist/runtime/annotation/engine/tools/rectangle.js +0 -2
  20. package/dist/runtime/annotation/engine/types.d.ts +9 -2
  21. package/dist/runtime/annotation/font-style.d.ts +15 -0
  22. package/dist/runtime/annotation/font-style.js +19 -0
  23. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +8 -0
  24. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +24 -1
  25. package/dist/runtime/annotation/pdf-export/annotation-flags.d.ts +8 -0
  26. package/dist/runtime/annotation/pdf-export/annotation-flags.js +5 -0
  27. package/dist/runtime/annotation/pdf-export/export-form-fields.js +3 -1
  28. package/dist/runtime/annotation/pdf-export/export.js +17 -4
  29. package/dist/runtime/annotation/pdf-export/parse.d.ts +22 -1
  30. package/dist/runtime/annotation/pdf-export/parse.js +32 -2
  31. package/dist/runtime/annotation/pdf-export/parse_circle.js +5 -5
  32. package/dist/runtime/annotation/pdf-export/parse_freetext.js +33 -24
  33. package/dist/runtime/annotation/pdf-export/parse_highlight.js +5 -5
  34. package/dist/runtime/annotation/pdf-export/parse_ink.js +7 -6
  35. package/dist/runtime/annotation/pdf-export/parse_line.js +7 -6
  36. package/dist/runtime/annotation/pdf-export/parse_polyline.js +7 -6
  37. package/dist/runtime/annotation/pdf-export/parse_square.js +5 -5
  38. package/dist/runtime/annotation/pdf-export/parse_stamp.js +6 -7
  39. package/dist/runtime/annotation/pdf-export/parse_strikeout.js +5 -5
  40. package/dist/runtime/annotation/pdf-export/parse_text.js +5 -5
  41. package/dist/runtime/annotation/pdf-export/parse_underline.js +5 -5
  42. package/dist/runtime/annotation/pdf-export/unicode-font.d.ts +5 -0
  43. package/dist/runtime/annotation/pdf-export/unicode-font.js +31 -0
  44. package/dist/runtime/annotation/pdf-export/winansi.d.ts +17 -0
  45. package/dist/runtime/annotation/pdf-export/winansi.js +77 -0
  46. package/dist/runtime/annotation/pdf-import/decode_circle.js +0 -1
  47. package/dist/runtime/annotation/pdf-import/decode_ink.js +0 -1
  48. package/dist/runtime/annotation/pdf-import/decode_line.js +0 -1
  49. package/dist/runtime/annotation/pdf-import/decode_square.js +0 -1
  50. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +12 -0
  51. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +17 -4
  52. package/dist/runtime/annotation/pdf-import/types.d.ts +2 -0
  53. package/dist/runtime/assets/fonts/LICENSE-LiberationSans +102 -0
  54. package/dist/runtime/assets/fonts/LiberationSans-Regular.ttf +0 -0
  55. package/dist/runtime/assets/kviewer.css +1 -1
  56. package/dist/runtime/components/AnnotationToolbar.vue +6 -4
  57. package/dist/runtime/components/PdfPage.d.vue.ts +16 -0
  58. package/dist/runtime/components/PdfPage.vue +170 -9
  59. package/dist/runtime/components/PdfPage.vue.d.ts +16 -0
  60. package/dist/runtime/components/PerfHud.d.vue.ts +3 -0
  61. package/dist/runtime/components/PerfHud.vue +97 -0
  62. package/dist/runtime/components/PerfHud.vue.d.ts +3 -0
  63. package/dist/runtime/components/ToolButton.vue +4 -1
  64. package/dist/runtime/components/Viewer.d.vue.ts +25 -2
  65. package/dist/runtime/components/Viewer.vue +174 -12
  66. package/dist/runtime/components/Viewer.vue.d.ts +25 -2
  67. package/dist/runtime/components/ViewerBar.d.vue.ts +21 -1
  68. package/dist/runtime/components/ViewerBar.vue +183 -101
  69. package/dist/runtime/components/ViewerBar.vue.d.ts +21 -1
  70. package/dist/runtime/components/ViewerTabs.d.vue.ts +10 -2
  71. package/dist/runtime/components/ViewerTabs.vue +13 -2
  72. package/dist/runtime/components/ViewerTabs.vue.d.ts +10 -2
  73. package/dist/runtime/components/form-fields/FormDropdown.vue +50 -19
  74. package/dist/runtime/components/form-fields/FormSignatureField.vue +4 -2
  75. package/dist/runtime/components/form-fields/FormTextField.vue +7 -18
  76. package/dist/runtime/components/modals/FreeTextModal.vue +6 -4
  77. package/dist/runtime/components/modals/SignatureDrawModal.vue +6 -4
  78. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +67 -50
  79. package/dist/runtime/components/panels/SignaturePicker.vue +6 -4
  80. package/dist/runtime/components/panels/StampPicker.vue +3 -1
  81. package/dist/runtime/components/tools/AnnotationToolButton.d.vue.ts +10 -0
  82. package/dist/runtime/components/tools/AnnotationToolButton.vue +33 -0
  83. package/dist/runtime/components/tools/AnnotationToolButton.vue.d.ts +10 -0
  84. package/dist/runtime/components/tools/MarqueeTool.vue +3 -1
  85. package/dist/runtime/components/tools/PageSettings.vue +13 -11
  86. package/dist/runtime/components/tools/SearchTool.vue +12 -10
  87. package/dist/runtime/components/tools/ToolProperties.vue +7 -5
  88. package/dist/runtime/components/tools/ToolbarMenu.d.vue.ts +9 -0
  89. package/dist/runtime/components/tools/ToolbarMenu.vue +79 -0
  90. package/dist/runtime/components/tools/ToolbarMenu.vue.d.ts +9 -0
  91. package/dist/runtime/components/tools/ZoomControls.vue +17 -9
  92. package/dist/runtime/composables/canvas-limits.d.ts +22 -0
  93. package/dist/runtime/composables/canvas-limits.js +10 -0
  94. package/dist/runtime/composables/useAnnotationEngine.js +3 -1
  95. package/dist/runtime/composables/useFormFields.d.ts +6 -0
  96. package/dist/runtime/composables/useFormFields.js +28 -0
  97. package/dist/runtime/composables/useInertiaPanzoom.d.ts +10 -0
  98. package/dist/runtime/composables/useInertiaPanzoom.js +57 -10
  99. package/dist/runtime/composables/useKviewerI18n.d.ts +18 -0
  100. package/dist/runtime/composables/useKviewerI18n.js +29 -0
  101. package/dist/runtime/composables/useScriptingManager.d.ts +8 -4
  102. package/dist/runtime/composables/useScriptingManager.js +2 -0
  103. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  104. package/dist/runtime/composables/useViewerState.js +8 -0
  105. package/dist/runtime/i18n/messages.d.ts +252 -0
  106. package/dist/runtime/i18n/messages.js +249 -0
  107. package/dist/runtime/public-types.d.ts +2 -0
  108. package/dist/runtime/toolbar-tools.d.ts +54 -0
  109. package/dist/runtime/toolbar-tools.js +79 -0
  110. package/dist/types.d.mts +1 -1
  111. package/package.json +14 -31
package/README.md CHANGED
@@ -71,6 +71,25 @@ const pdfUrl = '/documents/sample.pdf'
71
71
 
72
72
  `source` accepts a URL string, `Uint8Array`, or a pdfjs-dist source object.
73
73
 
74
+ ## Customizing the toolbar
75
+
76
+ By default the toolbar is fully built-in. Pass `tools` to choose which buttons render and in what order, and interleave custom buttons via `#tool-<name>` slots. Each entry is a built-in tool ID (a string) or a `{ type: 'slot' }` object. When omitted, the default toolbar is rendered unchanged.
77
+
78
+ ```vue
79
+ <template>
80
+ <KViewer
81
+ :source="pdfUrl"
82
+ :tools="['menu', 'pageSettings', 'separator', 'zoom', { type: 'slot', key: 'save' }, 'spacer', 'search']"
83
+ >
84
+ <template #tool-save="{ state }">
85
+ <UButton icon="i-lucide-save" size="xs" variant="ghost" color="neutral" @click="save(state)" />
86
+ </template>
87
+ </KViewer>
88
+ </template>
89
+ ```
90
+
91
+ `separator`/`spacer` are layout primitives (`spacer` right-aligns what follows). `tools` controls buttons only — it never gates programmatic `state.selectTool(...)` access. Form-edit mode is API-only (`v-model:form-edit-mode` / `setFormEditMode()` / `toggleFormEditMode()`); there is no built-in toggle button. See the [toolbar configuration docs](https://github.com/uppt/nuxt-kviewer) for the full tool-ID list and the array that reproduces the default toolbar.
92
+
74
93
  ## Viewer Ref API
75
94
 
76
95
  `KViewer` exposes methods via a template ref:
package/dist/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- export { AddFormFieldPayload, CheckboxStyle, ExportPdfOptions, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, KVIEWER_EMBED_PROTOCOL_VERSION, KViewerApi, KViewerEmbedBridgeOptions, KViewerEmbedClient, KViewerEmbedClientOptions, KViewerEmbedEventName, KViewerEmbedEventPayloads, KViewerEmbedMethodName, SignatureData, SignatureHandlers, ViewMode, ViewerMenuButtonItem, ViewerMenuCheckboxItem, ViewerMenuItem, ViewerMenuSeparatorItem, useKViewerEmbedBridge } from '../dist/runtime/public-types.js';
2
+ export { ActiveTool, AddFormFieldPayload, CheckboxStyle, ExportPdfOptions, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, KVIEWER_EMBED_PROTOCOL_VERSION, KViewerApi, KViewerEmbedBridgeOptions, KViewerEmbedClient, KViewerEmbedClientOptions, KViewerEmbedEventName, KViewerEmbedEventPayloads, KViewerEmbedMethodName, SignatureData, SignatureHandlers, ViewMode, ViewerBottomToolId, ViewerLayoutToolId, ViewerMenuButtonItem, ViewerMenuCheckboxItem, ViewerMenuItem, ViewerMenuSeparatorItem, ViewerState, ViewerToolEntry, ViewerToolId, ViewerToolSlot, ViewerTopToolId, useKViewerEmbedBridge } from '../dist/runtime/public-types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -15,6 +15,13 @@ interface ModuleOptions {
15
15
  * @defaultValue `2`
16
16
  */
17
17
  minRenderPixelRatio?: number;
18
+ /**
19
+ * Default UI language for built-in viewer strings (toolbar, search,
20
+ * modals, form sidebar). The Viewer `locale` prop overrides this
21
+ * per instance.
22
+ * @defaultValue `en`
23
+ */
24
+ locale?: 'en' | 'de';
18
25
  }
19
26
 
20
27
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -11,7 +11,8 @@ const module$1 = defineNuxtModule({
11
11
  // Default configuration options of the Nuxt module
12
12
  defaults: {
13
13
  prefix: "K",
14
- minRenderPixelRatio: 2
14
+ minRenderPixelRatio: 2,
15
+ locale: "en"
15
16
  },
16
17
  // Declare @nuxt/ui as a hard dependency so Nuxt resolves it as a
17
18
  // top-level module before our setup() runs. We can NOT just call
@@ -38,10 +39,11 @@ const module$1 = defineNuxtModule({
38
39
  "pdfjs-dist/legacy/build/pdf.mjs",
39
40
  "pdfjs-dist/web/pdf_viewer.mjs"
40
41
  );
41
- }, { client: true, server: false });
42
+ });
42
43
  nuxt.options.runtimeConfig.public.kviewer = {
43
44
  ...nuxt.options.runtimeConfig.public.kviewer,
44
- minRenderPixelRatio: options.minRenderPixelRatio ?? 2
45
+ minRenderPixelRatio: options.minRenderPixelRatio ?? 2,
46
+ locale: options.locale ?? "en"
45
47
  };
46
48
  nuxt.options.css.push("pdfjs-dist/web/pdf_viewer.css");
47
49
  nuxt.options.css.push(resolve("./runtime/assets/kviewer.css"));
@@ -80,6 +82,26 @@ const module$1 = defineNuxtModule({
80
82
  dir: join(pdfjsDir, "legacy/build"),
81
83
  baseURL: "/_kviewer/pdfjs",
82
84
  maxAge: 60 * 60 * 24 * 365
85
+ },
86
+ // pdf.js v6 moved several features to WebAssembly, fetched at
87
+ // runtime from the `wasmUrl` API option (see Viewer.vue) and the
88
+ // scripting `wasmUrl` (see useScriptingManager): quickjs-eval.wasm
89
+ // backs the QuickJS scripting sandbox (no JS fallback — required
90
+ // for `scripting`), while jbig2/openjpeg/qcms.wasm (with bundled
91
+ // *_nowasm_fallback.js) decode JBIG2 / JPEG2000 images and ICC
92
+ // colour. Without this, scripting fails and those image/colour
93
+ // features silently degrade.
94
+ {
95
+ dir: join(pdfjsDir, "wasm"),
96
+ baseURL: "/_kviewer/wasm",
97
+ maxAge: 60 * 60 * 24 * 365
98
+ },
99
+ // CMYK ICC profile fetched via the `iccUrl` API option for
100
+ // DeviceCMYK ICC-based colour spaces.
101
+ {
102
+ dir: join(pdfjsDir, "iccs"),
103
+ baseURL: "/_kviewer/iccs",
104
+ maxAge: 60 * 60 * 24 * 365
83
105
  }
84
106
  );
85
107
  });
@@ -0,0 +1,7 @@
1
+ import type Konva from 'konva';
2
+ /**
3
+ * Cap the backing-store resolution of every layer (scene + hit canvas)
4
+ * on the stage. Idempotent — call after adding a layer to a stage whose
5
+ * CSS size scales with zoom.
6
+ */
7
+ export declare function applyStageResolutionCap(stage: Konva.Stage): void;
@@ -0,0 +1,21 @@
1
+ import { isIPad } from "./input-device.js";
2
+ const MAX_LAYER_PIXELS_IPAD = 1e6;
3
+ const MAX_LAYER_PIXELS_DEFAULT = 8e6;
4
+ const baseRatios = /* @__PURE__ */ new WeakMap();
5
+ function capCanvas(canvas, cssArea, maxPixels) {
6
+ const current = canvas.getPixelRatio();
7
+ if (!baseRatios.has(canvas)) baseRatios.set(canvas, current);
8
+ const base = baseRatios.get(canvas);
9
+ const target = Math.min(base, Math.sqrt(maxPixels / cssArea));
10
+ if (Math.abs(target - current) > 0.01) canvas.setPixelRatio(target);
11
+ }
12
+ export function applyStageResolutionCap(stage) {
13
+ const cssArea = stage.width() * stage.height();
14
+ if (cssArea <= 0) return;
15
+ const maxPixels = isIPad() ? MAX_LAYER_PIXELS_IPAD : MAX_LAYER_PIXELS_DEFAULT;
16
+ for (const layer of stage.getLayers()) {
17
+ capCanvas(layer.getCanvas(), cssArea, maxPixels);
18
+ capCanvas(layer.getHitCanvas(), cssArea, maxPixels);
19
+ layer.batchDraw();
20
+ }
21
+ }
@@ -2,7 +2,7 @@ import Konva from 'konva';
2
2
  import type { KonvaEventObject } from 'konva/lib/Node';
3
3
  import type { AnnotationType, IAnnotationContentsObj, IAnnotationStore, IAnnotationStyle, IAnnotationType } from '../types.js';
4
4
  export interface IEditorOptions {
5
- userName: string;
5
+ userName?: string;
6
6
  konvaStage: Konva.Stage;
7
7
  pageNumber: number;
8
8
  annotation: IAnnotationType | null;
@@ -21,7 +21,7 @@ export interface IShapeGroup {
21
21
  annotation?: IAnnotationType;
22
22
  }
23
23
  export declare abstract class Editor {
24
- protected userName: string;
24
+ protected userName?: string;
25
25
  readonly id: string;
26
26
  readonly onAdd: (annotationStore: IAnnotationStore) => void;
27
27
  readonly onChange: (id: string, updates: Partial<IAnnotationStore>) => void;
@@ -72,4 +72,9 @@ export declare abstract class Editor {
72
72
  static TimerStart(pageNumber: number, callback: (pageNumber: number) => void, delay?: number): void;
73
73
  /** Immediately execute and clear all pending timers (e.g. before export). */
74
74
  static flushAllTimers(): void;
75
+ /** Immediately execute and clear one page's pending timer — e.g. before
76
+ * the page's Konva stage is destroyed (zoom commit, unmount), so an
77
+ * in-flight freehand group is finalized into the store instead of
78
+ * dying with the stage. */
79
+ static TimerFlush(pageNumber: number): void;
75
80
  }
@@ -213,6 +213,9 @@ export class Editor {
213
213
  addSerializedGroupToLayer(konvaStage, konvaString) {
214
214
  this.konvaStage = konvaStage;
215
215
  const ghostGroup = Konva.Node.create(konvaString);
216
+ if (ghostGroup instanceof Konva.Container) {
217
+ ghostGroup.find((node) => node instanceof Konva.Shape).forEach((shape) => shape.strokeScaleEnabled(true));
218
+ }
216
219
  const id = ghostGroup.id();
217
220
  this.getBgLayer(konvaStage).add(ghostGroup);
218
221
  if (this.shapeGroupStore.has(id)) return;
@@ -258,4 +261,15 @@ export class Editor {
258
261
  entry.callback(pageNumber);
259
262
  }
260
263
  }
264
+ /** Immediately execute and clear one page's pending timer — e.g. before
265
+ * the page's Konva stage is destroyed (zoom commit, unmount), so an
266
+ * in-flight freehand group is finalized into the store instead of
267
+ * dying with the stage. */
268
+ static TimerFlush(pageNumber) {
269
+ const entry = Editor.Timer.get(pageNumber);
270
+ if (!entry) return;
271
+ window.clearTimeout(entry.id);
272
+ Editor.Timer.delete(pageNumber);
273
+ entry.callback(pageNumber);
274
+ }
261
275
  }
@@ -6,6 +6,13 @@ export interface ISelectorOptions {
6
6
  getAnnotationStore: (id: string) => IAnnotationStore | undefined;
7
7
  onSelected: (id: string, isClick: boolean, transformerRect: IRect) => void;
8
8
  onMultiSelected: (ids: string[], combinedRect: IRect) => void;
9
+ /** Fired once after a marquee multi-drag with the final geometry of
10
+ * every moved group, so the move persists as a single history step. */
11
+ onMultiChanged: (changes: Array<{
12
+ id: string;
13
+ konvaGroupString: string;
14
+ konvaClientRect: IRect;
15
+ }>, transformerRect: IRect) => void;
9
16
  onCancel: () => void;
10
17
  onChanged: (id: string, konvaGroupString: string, rawAnnotationStore: IAnnotationStore, konvaClientRect: IRect, transformerRect: IRect) => void;
11
18
  onDelete: (id: string) => void;
@@ -27,6 +34,11 @@ export declare class Selector {
27
34
  readonly onDelete: (id: string) => void;
28
35
  readonly onCancel: () => void;
29
36
  readonly onMultiSelected: (ids: string[], combinedRect: IRect) => void;
37
+ readonly onMultiChanged: (changes: Array<{
38
+ id: string;
39
+ konvaGroupString: string;
40
+ konvaClientRect: IRect;
41
+ }>, transformerRect: IRect) => void;
30
42
  private onRequestDeleteConfirm?;
31
43
  private onFreeTextDoubleClick?;
32
44
  private getStylusModeEnabled?;
@@ -40,7 +52,7 @@ export declare class Selector {
40
52
  private marqueeRect;
41
53
  private marqueeStartPos;
42
54
  private marqueeStage;
43
- constructor({ konvaCanvasStore, getAnnotationStore, onDelete, onSelected, onMultiSelected, onCancel, onChanged, onRequestDeleteConfirm, onFreeTextDoubleClick, getStylusModeEnabled, setExternalGesturesEnabled, }: ISelectorOptions);
55
+ constructor({ konvaCanvasStore, getAnnotationStore, onDelete, onSelected, onMultiSelected, onMultiChanged, onCancel, onChanged, onRequestDeleteConfirm, onFreeTextDoubleClick, getStylusModeEnabled, setExternalGesturesEnabled, }: ISelectorOptions);
44
56
  /** Returns true if this event should be rejected (finger touch in stylus mode). */
45
57
  private isStylusRejected;
46
58
  private handleKeyDown;
@@ -3,12 +3,14 @@ import { AnnotationType } from "../types.js";
3
3
  import { SELECTOR_HOVER_STYLE, SHAPE_GROUP_NAME } from "../const.js";
4
4
  import { defaultOptions } from "../config.js";
5
5
  import { isStylusTouch } from "../input-device.js";
6
+ import { applyStageResolutionCap } from "../canvas-resolution.js";
6
7
  export class Selector {
7
8
  onSelected;
8
9
  onChanged;
9
10
  onDelete;
10
11
  onCancel;
11
12
  onMultiSelected;
13
+ onMultiChanged;
12
14
  onRequestDeleteConfirm;
13
15
  onFreeTextDoubleClick;
14
16
  getStylusModeEnabled;
@@ -28,6 +30,7 @@ export class Selector {
28
30
  onDelete,
29
31
  onSelected,
30
32
  onMultiSelected,
33
+ onMultiChanged,
31
34
  onCancel,
32
35
  onChanged,
33
36
  onRequestDeleteConfirm,
@@ -40,6 +43,7 @@ export class Selector {
40
43
  this.onDelete = onDelete;
41
44
  this.onSelected = onSelected;
42
45
  this.onMultiSelected = onMultiSelected;
46
+ this.onMultiChanged = onMultiChanged;
43
47
  this.onCancel = onCancel;
44
48
  this.onChanged = onChanged;
45
49
  this.onRequestDeleteConfirm = onRequestDeleteConfirm;
@@ -231,8 +235,9 @@ export class Selector {
231
235
  const moveToDragLayer = () => {
232
236
  if (dragLayer) return;
233
237
  const bg = this.getBackgroundLayer(konvaStage);
234
- dragLayer = new Konva.Layer();
238
+ dragLayer = new Konva.Layer({ listening: false });
235
239
  konvaStage.add(dragLayer);
240
+ applyStageResolutionCap(konvaStage);
236
241
  group.moveTo(dragLayer);
237
242
  transformer.moveTo(dragLayer);
238
243
  bg.batchDraw();
@@ -240,6 +245,7 @@ export class Selector {
240
245
  };
241
246
  const restoreFromDragLayer = () => {
242
247
  if (!dragLayer) return;
248
+ endGhostDrag();
243
249
  const bg = this.getBackgroundLayer(konvaStage);
244
250
  group.moveTo(bg);
245
251
  transformer.moveTo(bg);
@@ -247,6 +253,32 @@ export class Selector {
247
253
  dragLayer = null;
248
254
  bg.batchDraw();
249
255
  };
256
+ let dragGhost = null;
257
+ let dragVisualOrigin = null;
258
+ const startGhostDrag = (origin) => {
259
+ if (!dragLayer || dragGhost) return;
260
+ dragLayer.draw();
261
+ const canvases = konvaStage.content.querySelectorAll("canvas");
262
+ const real = canvases[canvases.length - 1];
263
+ if (!real) return;
264
+ const ghost = document.createElement("canvas");
265
+ ghost.width = real.width;
266
+ ghost.height = real.height;
267
+ ghost.getContext("2d")?.drawImage(real, 0, 0);
268
+ ghost.style.cssText = `position:absolute;top:0;left:0;width:${real.style.width};height:${real.style.height};pointer-events:none;`;
269
+ konvaStage.content.appendChild(ghost);
270
+ dragGhost = ghost;
271
+ dragVisualOrigin = origin;
272
+ dragLayer.visible(false);
273
+ };
274
+ const endGhostDrag = () => {
275
+ dragVisualOrigin = null;
276
+ if (!dragGhost) return;
277
+ dragLayer?.visible(true);
278
+ dragLayer?.draw();
279
+ dragGhost.remove();
280
+ dragGhost = null;
281
+ };
250
282
  transformer.on("transformstart", () => {
251
283
  this.setExternalGesturesEnabled?.(false);
252
284
  moveToDragLayer();
@@ -273,9 +305,13 @@ export class Selector {
273
305
  dragStartRect = this.getTotalBox(boxes);
274
306
  const firstNode = transformer.nodes()[0];
275
307
  dragAnchorPos = firstNode ? firstNode.getAbsolutePosition() : null;
308
+ if (transformer.nodes().length === 1 && dragAnchorPos) {
309
+ startGhostDrag({ ...dragAnchorPos });
310
+ }
276
311
  });
277
312
  transformer.on("dragend", () => {
278
313
  this.setExternalGesturesEnabled?.(true);
314
+ endGhostDrag();
279
315
  restoreFromDragLayer();
280
316
  dragStartRect = null;
281
317
  dragAnchorPos = null;
@@ -299,14 +335,17 @@ export class Selector {
299
335
  const boxY = dragStartRect.y + dy;
300
336
  const halfWidth = dragStartRect.width / 2;
301
337
  const halfHeight = dragStartRect.height / 2;
302
- const stageW = konvaStage.width();
303
- const stageH = konvaStage.height();
338
+ const pageCss = konvaStage.getAttr("kvPageCss");
339
+ const minX = konvaStage.x();
340
+ const minY = konvaStage.y();
341
+ const maxX = minX + (pageCss?.width ?? konvaStage.width());
342
+ const maxY = minY + (pageCss?.height ?? konvaStage.height());
304
343
  let clampDx = 0;
305
344
  let clampDy = 0;
306
- if (boxX + halfWidth < 0) clampDx = -(boxX + halfWidth);
307
- else if (boxX + halfWidth > stageW) clampDx = stageW - (boxX + halfWidth);
308
- if (boxY + halfHeight < 0) clampDy = -(boxY + halfHeight);
309
- else if (boxY + halfHeight > stageH) clampDy = stageH - (boxY + halfHeight);
345
+ if (boxX + halfWidth < minX) clampDx = minX - (boxX + halfWidth);
346
+ else if (boxX + halfWidth > maxX) clampDx = maxX - (boxX + halfWidth);
347
+ if (boxY + halfHeight < minY) clampDy = minY - (boxY + halfHeight);
348
+ else if (boxY + halfHeight > maxY) clampDy = maxY - (boxY + halfHeight);
310
349
  if (clampDx !== 0 || clampDy !== 0) {
311
350
  nodes.forEach((shape) => {
312
351
  const pos = shape.getAbsolutePosition();
@@ -314,6 +353,10 @@ export class Selector {
314
353
  });
315
354
  dragAnchorPos = firstNode.getAbsolutePosition();
316
355
  }
356
+ if (dragGhost && dragVisualOrigin) {
357
+ const pos = firstNode.getAbsolutePosition();
358
+ dragGhost.style.transform = `translate(${pos.x - dragVisualOrigin.x}px, ${pos.y - dragVisualOrigin.y}px)`;
359
+ }
317
360
  });
318
361
  transformer.nodes([group]);
319
362
  this.getBackgroundLayer(konvaStage).add(transformer);
@@ -398,6 +441,7 @@ export class Selector {
398
441
  transformer.nodes().forEach((group) => {
399
442
  if (group instanceof Konva.Group) {
400
443
  group.draggable(false);
444
+ group.off(".marquee");
401
445
  }
402
446
  });
403
447
  transformer.nodes([]);
@@ -555,6 +599,12 @@ export class Selector {
555
599
  });
556
600
  node.on("dragend.marquee", () => {
557
601
  this.setExternalGesturesEnabled?.(true);
602
+ const changes = transformer.nodes().filter((n) => n instanceof Konva.Group).map((n) => ({
603
+ id: n.id(),
604
+ konvaGroupString: n.toJSON(),
605
+ konvaClientRect: Konva.Node.create(n.toJSON()).getClientRect()
606
+ }));
607
+ this.onMultiChanged(changes, transformer.getClientRect());
558
608
  this.onMultiSelected(matchedIds, transformer.getClientRect());
559
609
  });
560
610
  });
@@ -673,7 +723,9 @@ export class Selector {
673
723
  return;
674
724
  }
675
725
  const scale = this.eraserStage.scaleX() ?? 1;
676
- const screenPoints = points.map((v) => v * scale);
726
+ const sx = this.eraserStage.x();
727
+ const sy = this.eraserStage.y();
728
+ const screenPoints = points.map((v, i) => v * scale + (i % 2 === 0 ? sx : sy));
677
729
  const tolerance = 6;
678
730
  const groups = this.getPageShapeGroups(this.eraserStage);
679
731
  const toDelete = [];
@@ -21,7 +21,7 @@ function normalizeComment(comment, fallbackTitle) {
21
21
  if (!isString(content)) return null;
22
22
  return {
23
23
  id: isString(comment.id) && comment.id.length > 0 ? comment.id : generateUUID(),
24
- title: isString(comment.title) && comment.title.length > 0 ? comment.title : fallbackTitle,
24
+ title: isString(comment.title) && comment.title.length > 0 ? comment.title : fallbackTitle || void 0,
25
25
  date: isString(comment.date) && comment.date.length > 0 ? comment.date : formatTimestamp(Date.now()),
26
26
  content,
27
27
  status: isString(comment.status) ? comment.status : void 0
@@ -66,7 +66,7 @@ function normalizeAnnotation(raw, fallbackTitle) {
66
66
  pageNumber,
67
67
  konvaString,
68
68
  konvaClientRect: { x, y, width, height },
69
- title: isString(raw.title) && raw.title.length > 0 ? raw.title : fallbackTitle,
69
+ title: isString(raw.title) && raw.title.length > 0 ? raw.title : fallbackTitle || void 0,
70
70
  type,
71
71
  subtype,
72
72
  pdfjsType,
@@ -81,7 +81,7 @@ function normalizeAnnotation(raw, fallbackTitle) {
81
81
  };
82
82
  return normalized;
83
83
  }
84
- export function normalizeImportedAnnotations(input, fallbackTitle = "User") {
84
+ export function normalizeImportedAnnotations(input, fallbackTitle) {
85
85
  if (!Array.isArray(input)) {
86
86
  return { annotations: [], skipped: 0 };
87
87
  }
@@ -29,10 +29,14 @@ export interface PainterCallbacks {
29
29
  getFormFieldPreviewColor?: () => string | undefined;
30
30
  }
31
31
  export declare class Painter {
32
- private userName;
32
+ private userName?;
33
33
  private konvaCanvasStore;
34
34
  private editorStore;
35
35
  private currentAnnotation;
36
+ /** Marquee/eraser set currentAnnotation to null, so the active mode is
37
+ * tracked separately to allow restoring its bindings after programmatic
38
+ * editor churn (undo/redo, import). */
39
+ private activeNonToolMode;
36
40
  private store;
37
41
  private selector;
38
42
  private tempDataTransfer;
@@ -41,7 +45,7 @@ export declare class Painter {
41
45
  private getReadonly?;
42
46
  private freehandGroupingDelay;
43
47
  constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, freehandGroupingDelay, setExternalGesturesEnabled, }: {
44
- userName: string;
48
+ userName?: string;
45
49
  callbacks: PainterCallbacks;
46
50
  getStylusModeEnabled?: () => boolean;
47
51
  getReadonly?: () => boolean;
@@ -52,6 +56,21 @@ export declare class Painter {
52
56
  private bindGlobalEvents;
53
57
  private globalKeyUpHandler;
54
58
  private createKonvaStage;
59
+ /**
60
+ * Restrict the stage to the visible window of the page (page-CSS
61
+ * coords) — or restore the full page with `null`. iOS Safari blits a
62
+ * changing canvas at its CSS size in software: a page-sized canvas at
63
+ * high zoom costs hundreds of ms per frame on A-series iPads no matter
64
+ * how small the backing store is. Shrinking the stage CSS to the
65
+ * viewport window makes drag cost viewport-bound. The stage position
66
+ * offsets the content so shapes keep page coordinates.
67
+ */
68
+ setStageWindow(pageNumber: number, win: {
69
+ x: number;
70
+ y: number;
71
+ width: number;
72
+ height: number;
73
+ } | null): void;
55
74
  private applyStylusTouchAction;
56
75
  private removeStylusTouchAction;
57
76
  private saveToStore;
@@ -89,6 +108,18 @@ export declare class Painter {
89
108
  * is currently active.
90
109
  */
91
110
  private refreshSelectorForPages;
111
+ /**
112
+ * Re-bind the stage handlers of whatever tool is currently active.
113
+ *
114
+ * Creating or activating an editor rebinds the Konva stage events to
115
+ * THAT editor (the Editor constructor and activate() both run
116
+ * disableEditMode + enableEditMode). Programmatic add paths — undo/redo
117
+ * via reAddAnnotation, importAnnotations via merge/replace — may lazily
118
+ * create editors for the restored annotation's type, which would leave
119
+ * the stage bound to the wrong tool (e.g. undoing a rectangle delete
120
+ * while freehand is active made the stage draw rectangles).
121
+ */
122
+ private restoreActiveToolBindings;
92
123
  refreshStylusMode(): void;
93
124
  initCanvasForPage(pageNumber: number, container: HTMLDivElement, width: number, height: number, scale: number): void;
94
125
  destroyCanvasForPage(pageNumber: number): void;
@@ -26,6 +26,7 @@ import { EditorFormField } from "./tools/form-field.js";
26
26
  import { Selector } from "./editor/selector.js";
27
27
  import { Store } from "./store.js";
28
28
  import { isIPad } from "./input-device.js";
29
+ import { applyStageResolutionCap } from "./canvas-resolution.js";
29
30
  if (typeof window !== "undefined" && isIPad()) {
30
31
  Konva.pixelRatio = 1;
31
32
  }
@@ -34,6 +35,10 @@ export class Painter {
34
35
  konvaCanvasStore = /* @__PURE__ */ new Map();
35
36
  editorStore = /* @__PURE__ */ new Map();
36
37
  currentAnnotation = null;
38
+ /** Marquee/eraser set currentAnnotation to null, so the active mode is
39
+ * tracked separately to allow restoring its bindings after programmatic
40
+ * editor churn (undo/redo, import). */
41
+ activeNonToolMode = null;
37
42
  store;
38
43
  selector;
39
44
  tempDataTransfer = null;
@@ -83,6 +88,21 @@ export class Painter {
83
88
  this.callbacks.onAnnotationChanged(ann, viewportRect);
84
89
  }
85
90
  },
91
+ onMultiChanged: (changes, transformerRect) => {
92
+ let lastChanged = null;
93
+ for (const change of changes) {
94
+ if (!this.store.annotation(change.id)) continue;
95
+ this.updateStore(change.id, {
96
+ konvaString: change.konvaGroupString,
97
+ konvaClientRect: change.konvaClientRect
98
+ });
99
+ lastChanged = this.store.annotation(change.id) ?? lastChanged;
100
+ }
101
+ if (lastChanged) {
102
+ const viewportRect = this.toViewportRect(lastChanged.pageNumber, transformerRect);
103
+ this.callbacks.onAnnotationChanged(lastChanged, viewportRect);
104
+ }
105
+ },
86
106
  onCancel: () => {
87
107
  this.callbacks.onAnnotationChanging();
88
108
  },
@@ -137,13 +157,46 @@ export class Painter {
137
157
  height: Math.floor(height * scale),
138
158
  scale: { x: scale, y: scale }
139
159
  });
160
+ stage.setAttr("kvPageCss", {
161
+ width: Math.floor(width * scale),
162
+ height: Math.floor(height * scale)
163
+ });
140
164
  const backgroundLayer = new Konva.Layer();
141
165
  stage.add(backgroundLayer);
166
+ applyStageResolutionCap(stage);
142
167
  if (this.getStylusModeEnabled?.()) {
143
168
  this.applyStylusTouchAction(stage);
144
169
  }
145
170
  return stage;
146
171
  }
172
+ /**
173
+ * Restrict the stage to the visible window of the page (page-CSS
174
+ * coords) — or restore the full page with `null`. iOS Safari blits a
175
+ * changing canvas at its CSS size in software: a page-sized canvas at
176
+ * high zoom costs hundreds of ms per frame on A-series iPads no matter
177
+ * how small the backing store is. Shrinking the stage CSS to the
178
+ * viewport window makes drag cost viewport-bound. The stage position
179
+ * offsets the content so shapes keep page coordinates.
180
+ */
181
+ setStageWindow(pageNumber, win) {
182
+ const canvas = this.konvaCanvasStore.get(pageNumber);
183
+ if (!canvas) return;
184
+ const stage = canvas.konvaStage;
185
+ const pageCss = stage.getAttr("kvPageCss");
186
+ const x = win ? Math.max(0, Math.min(win.x, pageCss.width)) : 0;
187
+ const y = win ? Math.max(0, Math.min(win.y, pageCss.height)) : 0;
188
+ const w = win ? Math.min(win.x + win.width, pageCss.width) - x : pageCss.width;
189
+ const h = win ? Math.min(win.y + win.height, pageCss.height) - y : pageCss.height;
190
+ if (w <= 0 || h <= 0) return;
191
+ if (Math.abs(stage.width() - w) < 1 && Math.abs(stage.height() - h) < 1 && Math.abs(stage.x() + x) < 1 && Math.abs(stage.y() + y) < 1) return;
192
+ stage.size({ width: Math.ceil(w), height: Math.ceil(h) });
193
+ stage.position({ x: -x, y: -y });
194
+ stage.content.style.position = "absolute";
195
+ stage.content.style.left = `${x}px`;
196
+ stage.content.style.top = `${y}px`;
197
+ applyStageResolutionCap(stage);
198
+ stage.batchDraw();
199
+ }
147
200
  applyStylusTouchAction(stage) {
148
201
  stage.preventDefault(false);
149
202
  if (!isIPad()) {
@@ -190,8 +243,8 @@ export class Painter {
190
243
  toViewportRect(pageNumber, stageRect) {
191
244
  const canvas = this.konvaCanvasStore.get(pageNumber);
192
245
  if (!canvas) return stageRect;
193
- const containerBounds = canvas.wrapper.getBoundingClientRect();
194
- const visualScale = canvas.wrapper.offsetWidth > 0 ? containerBounds.width / canvas.wrapper.offsetWidth : 1;
246
+ const containerBounds = canvas.konvaStage.content.getBoundingClientRect();
247
+ const visualScale = canvas.konvaStage.width() > 0 ? containerBounds.width / canvas.konvaStage.width() : 1;
195
248
  return {
196
249
  x: containerBounds.left + stageRect.x * visualScale,
197
250
  y: containerBounds.top + stageRect.y * visualScale,
@@ -460,6 +513,32 @@ export class Painter {
460
513
  this.selector.activate(pageNumber);
461
514
  }
462
515
  }
516
+ /**
517
+ * Re-bind the stage handlers of whatever tool is currently active.
518
+ *
519
+ * Creating or activating an editor rebinds the Konva stage events to
520
+ * THAT editor (the Editor constructor and activate() both run
521
+ * disableEditMode + enableEditMode). Programmatic add paths — undo/redo
522
+ * via reAddAnnotation, importAnnotations via merge/replace — may lazily
523
+ * create editors for the restored annotation's type, which would leave
524
+ * the stage bound to the wrong tool (e.g. undoing a rectangle delete
525
+ * while freehand is active made the stage draw rectangles).
526
+ */
527
+ restoreActiveToolBindings() {
528
+ if (this.currentAnnotation) {
529
+ this.enablePainting();
530
+ return;
531
+ }
532
+ if (this.activeNonToolMode === "marquee") {
533
+ this.konvaCanvasStore.forEach(({ pageNumber }) => {
534
+ this.selector.activateMarquee(pageNumber);
535
+ });
536
+ } else if (this.activeNonToolMode === "eraser") {
537
+ this.konvaCanvasStore.forEach(({ pageNumber }) => {
538
+ this.selector.activateEraser(pageNumber);
539
+ });
540
+ }
541
+ }
463
542
  // --- Public API ---
464
543
  refreshStylusMode() {
465
544
  const enabled = this.getStylusModeEnabled?.() ?? false;
@@ -489,12 +568,14 @@ export class Painter {
489
568
  destroyCanvasForPage(pageNumber) {
490
569
  const konvaCanvas = this.konvaCanvasStore.get(pageNumber);
491
570
  if (konvaCanvas) {
571
+ Editor.TimerFlush(pageNumber);
492
572
  konvaCanvas.konvaStage.destroy();
493
573
  this.konvaCanvasStore.delete(pageNumber);
494
574
  }
495
575
  }
496
576
  activate(annotation, dataTransfer) {
497
577
  this.currentAnnotation = annotation;
578
+ this.activeNonToolMode = null;
498
579
  this.disablePainting();
499
580
  this.saveTempDataTransfer(dataTransfer);
500
581
  if (!annotation) return;
@@ -522,6 +603,7 @@ export class Painter {
522
603
  }
523
604
  activateMarquee() {
524
605
  this.currentAnnotation = null;
606
+ this.activeNonToolMode = "marquee";
525
607
  this.disablePainting();
526
608
  this.konvaCanvasStore.forEach(({ pageNumber }) => {
527
609
  this.selector.activateMarquee(pageNumber);
@@ -529,6 +611,7 @@ export class Painter {
529
611
  }
530
612
  activateEraser() {
531
613
  this.currentAnnotation = null;
614
+ this.activeNonToolMode = "eraser";
532
615
  this.disablePainting();
533
616
  this.konvaCanvasStore.forEach(({ pageNumber }) => {
534
617
  this.selector.activateEraser(pageNumber);
@@ -585,6 +668,7 @@ export class Painter {
585
668
  }
586
669
  if (editor) {
587
670
  editor.addSerializedGroupToLayer(konvaCanvas.konvaStage, annotation.konvaString);
671
+ this.restoreActiveToolBindings();
588
672
  this.refreshSelectorForPages([annotation.pageNumber]);
589
673
  }
590
674
  }
@@ -622,6 +706,7 @@ export class Painter {
622
706
  this.drawAnnotation(annotation);
623
707
  touchedPages.add(annotation.pageNumber);
624
708
  });
709
+ this.restoreActiveToolBindings();
625
710
  this.refreshSelectorForPages(touchedPages);
626
711
  }
627
712
  mergeAnnotations(annotations) {
@@ -634,6 +719,7 @@ export class Painter {
634
719
  this.drawAnnotation(annotation);
635
720
  touchedPages.add(annotation.pageNumber);
636
721
  });
722
+ this.restoreActiveToolBindings();
637
723
  this.refreshSelectorForPages(touchedPages);
638
724
  }
639
725
  getCanvasState() {