kviewer 0.3.2 → 0.3.4

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 (51) hide show
  1. package/dist/module.d.mts +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +15 -15
  4. package/dist/runtime/annotation/engine/editor/selector.d.ts +1 -0
  5. package/dist/runtime/annotation/engine/editor/selector.js +17 -5
  6. package/dist/runtime/annotation/engine/painter.d.ts +2 -0
  7. package/dist/runtime/annotation/engine/painter.js +4 -0
  8. package/dist/runtime/annotation/pdf-export/export-form-fields.js +26 -41
  9. package/dist/runtime/annotation/pdf-export/print.d.ts +12 -0
  10. package/dist/runtime/annotation/pdf-export/print.js +63 -0
  11. package/dist/runtime/assets/kviewer.css +1 -1
  12. package/dist/runtime/components/AnnotationToolbar.vue +12 -1
  13. package/dist/runtime/components/FormFieldLayer.vue +7 -2
  14. package/dist/runtime/components/PdfPage.vue +24 -1
  15. package/dist/runtime/components/SelectionToolbarItems.d.vue.ts +8 -0
  16. package/dist/runtime/components/SelectionToolbarItems.vue +43 -0
  17. package/dist/runtime/components/SelectionToolbarItems.vue.d.ts +8 -0
  18. package/dist/runtime/components/Viewer.d.vue.ts +17 -1
  19. package/dist/runtime/components/Viewer.vue +45 -2
  20. package/dist/runtime/components/Viewer.vue.d.ts +17 -1
  21. package/dist/runtime/components/ViewerBar.d.vue.ts +4 -4
  22. package/dist/runtime/components/ViewerBar.vue +2 -1
  23. package/dist/runtime/components/ViewerBar.vue.d.ts +4 -4
  24. package/dist/runtime/components/ViewerTabs.d.vue.ts +4 -0
  25. package/dist/runtime/components/ViewerTabs.vue +2 -0
  26. package/dist/runtime/components/ViewerTabs.vue.d.ts +4 -0
  27. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +15 -2
  28. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +9 -9
  29. package/dist/runtime/components/form-fields/PlacedFieldToolbar.d.vue.ts +7 -0
  30. package/dist/runtime/components/form-fields/PlacedFieldToolbar.vue +51 -0
  31. package/dist/runtime/components/form-fields/PlacedFieldToolbar.vue.d.ts +7 -0
  32. package/dist/runtime/components/tools/ToolbarMenu.vue +17 -0
  33. package/dist/runtime/composables/useScriptingManager.d.ts +5 -0
  34. package/dist/runtime/composables/useScriptingManager.js +6 -0
  35. package/dist/runtime/composables/useViewerState.d.ts +2 -0
  36. package/dist/runtime/composables/useViewerState.js +8 -0
  37. package/dist/runtime/embed/bridge-client.d.ts +4 -0
  38. package/dist/runtime/embed/bridge-client.js +6 -0
  39. package/dist/runtime/embed/bridge-host.d.ts +10 -1
  40. package/dist/runtime/embed/bridge-host.js +102 -28
  41. package/dist/runtime/embed/protocol.d.ts +17 -0
  42. package/dist/runtime/embed/protocol.js +1 -0
  43. package/dist/runtime/i18n/messages.d.ts +2 -0
  44. package/dist/runtime/i18n/messages.js +2 -0
  45. package/dist/runtime/public-types.d.ts +4 -3
  46. package/dist/runtime/public-types.js +4 -1
  47. package/dist/runtime/selection-items.d.ts +61 -0
  48. package/dist/runtime/selection-items.js +1 -0
  49. package/dist/runtime/toolbar-tools.d.ts +4 -1
  50. package/dist/types.d.mts +1 -1
  51. package/package.json +1 -1
@@ -193,7 +193,8 @@ import {
193
193
  triggerRef,
194
194
  onMounted,
195
195
  onBeforeUnmount,
196
- useSlots
196
+ useSlots,
197
+ provide
197
198
  } from "vue";
198
199
  import { useRuntimeConfig } from "#imports";
199
200
  import ViewerBar from "./ViewerBar.vue";
@@ -214,6 +215,7 @@ import {
214
215
  } from "../annotation/pdf-import/decode";
215
216
  import { getTimestampString } from "../annotation/engine/utils";
216
217
  import { downloadPdfBytes } from "../annotation/pdf-export/download";
218
+ import { printPdfBytes } from "../annotation/pdf-export/print";
217
219
  import { exportAnnotationsToPdf } from "../annotation/pdf-export/export";
218
220
  import { createPageVirtualization } from "../composables/usePageVirtualization";
219
221
  import { createPageProxyCache } from "../composables/usePageProxyCache";
@@ -225,6 +227,7 @@ import { isIPad } from "../annotation/engine/input-device";
225
227
  import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
226
228
  import { createScriptingManager } from "../composables/useScriptingManager";
227
229
  import { createScriptingBridge } from "../composables/useScriptingBridge";
230
+ import { SELECTION_ITEMS_KEY } from "../selection-items";
228
231
  import { provideKviewerI18n } from "../composables/useKviewerI18n";
229
232
  import { KVIEWER_LOCALES } from "../i18n/messages";
230
233
  const props = defineProps({
@@ -247,10 +250,12 @@ const props = defineProps({
247
250
  fieldDefaults: { type: Object, required: false, default: void 0 },
248
251
  scripting: { type: Boolean, required: false, default: false },
249
252
  menuItems: { type: Array, required: false, default: void 0 },
253
+ selectionItems: { type: Array, required: false, default: void 0 },
250
254
  tools: { type: Array, required: false, default: void 0 },
251
255
  locale: { type: null, required: false, default: void 0 },
252
256
  messages: { type: Object, required: false, default: void 0 }
253
257
  });
258
+ provide(SELECTION_ITEMS_KEY, computed(() => props.selectionItems));
254
259
  const slots = useSlots();
255
260
  const toolSlotNames = computed(
256
261
  () => Object.keys(slots).filter((name) => name.startsWith("tool-"))
@@ -260,7 +265,7 @@ const fieldPlacedCallbacks = /* @__PURE__ */ new Set();
260
265
  const signedStatusCallbacks = /* @__PURE__ */ new Set();
261
266
  const viewerRoot = ref(null);
262
267
  const scrollContainer = ref(null);
263
- const { state: viewerState, setScrollToPageFn, setDownloadPdfFn, setApplyScaleFn } = provideViewerState();
268
+ const { state: viewerState, setScrollToPageFn, setDownloadPdfFn, setPrintPdfFn, setApplyScaleFn } = provideViewerState();
264
269
  watchEffect(() => {
265
270
  viewerState.readonly.value = props.readonly ?? false;
266
271
  });
@@ -315,6 +320,25 @@ watch(
315
320
  },
316
321
  { immediate: true }
317
322
  );
323
+ watch(
324
+ () => formFieldsState.selectedPlacedFieldId.value,
325
+ (id) => {
326
+ if (!id) return;
327
+ viewerState.painter.value?.clearSelection();
328
+ viewerState.selectedAnnotation.value = null;
329
+ viewerState.selectedAnnotations.value = [];
330
+ viewerState.selectionRect.value = null;
331
+ }
332
+ );
333
+ watch(
334
+ [
335
+ () => viewerState.selectedAnnotation.value,
336
+ () => viewerState.selectedAnnotations.value
337
+ ],
338
+ ([single, multi]) => {
339
+ if (single || multi.length > 0) formFieldsState.selectPlacedField(null);
340
+ }
341
+ );
318
342
  const signatureStatus = computed(() => formFieldsState.getSignatureFieldStatus());
319
343
  const signatureStatusKey = (fields) => fields.map((s) => `${s.id}|${s.fieldName}|${s.signed ? 1 : 0}`).join(";");
320
344
  watch(signatureStatus, (next, prev) => {
@@ -806,6 +830,18 @@ async function exportPdf(options = {}) {
806
830
  }
807
831
  return bytes;
808
832
  }
833
+ async function printPdf(options = {}) {
834
+ if (scriptingManager?.isReady()) {
835
+ await scriptingManager.dispatchWillPrint().catch(() => {
836
+ });
837
+ }
838
+ const bytes = await exportPdf({ ...options, download: false });
839
+ await printPdfBytes(bytes);
840
+ if (scriptingManager?.isReady()) {
841
+ scriptingManager.dispatchDidPrint().catch(() => {
842
+ });
843
+ }
844
+ }
809
845
  async function importAnnotations(annotations, options) {
810
846
  const mode = options?.mode ?? "replace";
811
847
  const normalized = normalizeImportedAnnotations(
@@ -1025,6 +1061,9 @@ onMounted(() => {
1025
1061
  setDownloadPdfFn(async () => {
1026
1062
  await exportPdf({ download: true });
1027
1063
  });
1064
+ setPrintPdfFn(async () => {
1065
+ await printPdf();
1066
+ });
1028
1067
  loadDocument();
1029
1068
  window.addEventListener("keydown", onGlobalKeydown);
1030
1069
  });
@@ -1094,6 +1133,10 @@ defineExpose({
1094
1133
  getAnnotations: () => painter?.getData() ?? [],
1095
1134
  importAnnotations,
1096
1135
  exportPdf,
1136
+ /** Export the document (same options as `exportPdf`, minus `download`)
1137
+ * and open the browser's print dialog on the resulting vector PDF —
1138
+ * full print quality, unlike printing the screen canvases. */
1139
+ printPdf,
1097
1140
  getKonvaCanvasState,
1098
1141
  getFormFieldValues: () => formFieldsState.getAllFieldValues(),
1099
1142
  setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value),
@@ -3,6 +3,7 @@ import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, Placed
3
3
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
5
  import type { ViewerToolEntry } from '../toolbar-tools.js';
6
+ import { type ViewerSelectionItem } from '../selection-items.js';
6
7
  import { type KviewerMessageOverrides } from '../composables/useKviewerI18n.js';
7
8
  import { type KviewerLocale } from '../i18n/messages.js';
8
9
  type __VLS_Props = {
@@ -63,6 +64,11 @@ type __VLS_Props = {
63
64
  * toggles or actions without replacing the entire header. Ignored
64
65
  * when the host supplies a custom `#header` slot. */
65
66
  menuItems?: ViewerMenuItem[];
67
+ /** Custom controls appended to the floating selection popover — shown
68
+ * when an annotation or a placed form field (e.g. a signature field)
69
+ * is selected. Buttons and selects; every callback receives the
70
+ * current selection. See {@link ViewerSelectionItem}. */
71
+ selectionItems?: ViewerSelectionItem[];
66
72
  /** Configures which toolbar tools render and in what order. Entries are
67
73
  * built-in tool IDs (strings) or `{ type: 'slot', ... }` objects that
68
74
  * inject a custom button via a `#tool-<name>` slot. When omitted, the
@@ -78,6 +84,11 @@ type __VLS_Props = {
78
84
  messages?: KviewerMessageOverrides;
79
85
  };
80
86
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
87
+ /** Print via the real PDF instead of the on-screen canvases: export the
88
+ * document (annotations + form values baked in) and hand the vector bytes
89
+ * to the browser's native PDF print pipeline. Screen canvases are sized
90
+ * for the monitor and would come out blurry on paper. */
91
+ declare function printPdf(options?: ExportPdfOptions): Promise<void>;
81
92
  type ImportMode = 'replace' | 'merge';
82
93
  declare function importAnnotations(annotations: IAnnotationStore[], options?: {
83
94
  mode?: ImportMode;
@@ -100,6 +111,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
100
111
  getAnnotations: () => IAnnotationStore[];
101
112
  importAnnotations: typeof importAnnotations;
102
113
  exportPdf: typeof exportPdf;
114
+ /** Export the document (same options as `exportPdf`, minus `download`)
115
+ * and open the browser's print dialog on the resulting vector PDF —
116
+ * full print quality, unlike printing the screen canvases. */
117
+ printPdf: typeof printPdf;
103
118
  getKonvaCanvasState: typeof getKonvaCanvasState;
104
119
  getFormFieldValues: () => import("../annotation/engine/types.js").FormFieldValue[];
105
120
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
@@ -180,6 +195,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
180
195
  clickToSign: boolean;
181
196
  readonly: boolean;
182
197
  active: boolean;
198
+ formEditMode: boolean;
183
199
  locale: KviewerLocale;
184
200
  menuItems: ViewerMenuItem[];
185
201
  tools: ViewerToolEntry[];
@@ -188,11 +204,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
188
204
  signatureHandlers: SignatureHandlers;
189
205
  viewMode: ViewMode;
190
206
  shapeDetection: boolean;
191
- formEditMode: boolean;
192
207
  roleColors: Record<string, string>;
193
208
  activeRoleId: string | null;
194
209
  editablePlacedFields: boolean;
195
210
  fieldDefaults: PlacedFieldDefaults;
211
+ selectionItems: ViewerSelectionItem[];
196
212
  messages: KviewerMessageOverrides;
197
213
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
198
214
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -8,15 +8,15 @@ type __VLS_Props = {
8
8
  * omitted, the default toolbar is rendered. See {@link ViewerToolEntry}. */
9
9
  tools?: ViewerToolEntry[];
10
10
  };
11
- declare var __VLS_109: string, __VLS_110: {
11
+ declare var __VLS_104: string, __VLS_105: {
12
12
  state: import("../composables/useViewerState.js").ViewerState;
13
- }, __VLS_154: string, __VLS_155: {
13
+ }, __VLS_149: string, __VLS_150: {
14
14
  state: import("../composables/useViewerState.js").ViewerState;
15
15
  };
16
16
  type __VLS_Slots = {} & {
17
- [K in NonNullable<typeof __VLS_109>]?: (props: typeof __VLS_110) => any;
17
+ [K in NonNullable<typeof __VLS_104>]?: (props: typeof __VLS_105) => any;
18
18
  } & {
19
- [K in NonNullable<typeof __VLS_154>]?: (props: typeof __VLS_155) => any;
19
+ [K in NonNullable<typeof __VLS_149>]?: (props: typeof __VLS_150) => any;
20
20
  };
21
21
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -11,11 +11,12 @@
11
11
 
12
12
  <ZoomControls />
13
13
 
14
+ <!-- No MarqueeTool button: hand mode covers mouse rubber-band
15
+ selection; `marquee` stays available via the `tools` prop. -->
14
16
  <template v-if="!state.readonly.value">
15
17
  <USeparator orientation="vertical" class="h-5" :ui="{ border: 'border-accented' }" />
16
18
 
17
19
  <HandTool />
18
- <MarqueeTool />
19
20
  </template>
20
21
 
21
22
  <div class="ml-auto flex items-center gap-2">
@@ -8,15 +8,15 @@ type __VLS_Props = {
8
8
  * omitted, the default toolbar is rendered. See {@link ViewerToolEntry}. */
9
9
  tools?: ViewerToolEntry[];
10
10
  };
11
- declare var __VLS_109: string, __VLS_110: {
11
+ declare var __VLS_104: string, __VLS_105: {
12
12
  state: import("../composables/useViewerState.js").ViewerState;
13
- }, __VLS_154: string, __VLS_155: {
13
+ }, __VLS_149: string, __VLS_150: {
14
14
  state: import("../composables/useViewerState.js").ViewerState;
15
15
  };
16
16
  type __VLS_Slots = {} & {
17
- [K in NonNullable<typeof __VLS_109>]?: (props: typeof __VLS_110) => any;
17
+ [K in NonNullable<typeof __VLS_104>]?: (props: typeof __VLS_105) => any;
18
18
  } & {
19
- [K in NonNullable<typeof __VLS_154>]?: (props: typeof __VLS_155) => any;
19
+ [K in NonNullable<typeof __VLS_149>]?: (props: typeof __VLS_150) => any;
20
20
  };
21
21
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -2,6 +2,7 @@ import { type ComponentPublicInstance } from 'vue';
2
2
  import type { ViewMode } from '../composables/useViewerState.js';
3
3
  import type { FormFieldDefinition, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
+ import type { ViewerSelectionItem } from '../selection-items.js';
5
6
  import type { ViewerToolEntry } from '../toolbar-tools.js';
6
7
  export interface ViewerTabItem {
7
8
  /** Unique identifier for the tab. */
@@ -63,6 +64,8 @@ type __VLS_Props = {
63
64
  scripting?: boolean;
64
65
  /** Forwarded to every Viewer instance — see KViewer's `menuItems`. */
65
66
  menuItems?: ViewerMenuItem[];
67
+ /** Forwarded to every Viewer instance — see KViewer's `selectionItems`. */
68
+ selectionItems?: ViewerSelectionItem[];
66
69
  /** Forwarded to every Viewer instance — see KViewer's `tools`. */
67
70
  tools?: ViewerToolEntry[];
68
71
  /** Forwarded to every Viewer instance — see KViewer's `clickToSign`. */
@@ -436,6 +439,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
436
439
  activeRoleId: string | null;
437
440
  editablePlacedFields: boolean;
438
441
  fieldDefaults: PlacedFieldDefaults;
442
+ selectionItems: ViewerSelectionItem[];
439
443
  defaultActiveTab: string;
440
444
  minTabs: number;
441
445
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -61,6 +61,7 @@
61
61
  :shape-detection="activeTab.shapeDetection ?? shapeDetection"
62
62
  :scripting="activeTab.scripting ?? scripting"
63
63
  :menu-items="menuItems"
64
+ :selection-items="selectionItems"
64
65
  :tools="tools"
65
66
  :role-colors="roleColors"
66
67
  :active-role-id="activeRoleId"
@@ -112,6 +113,7 @@ const props = defineProps({
112
113
  activeRoleId: { type: [String, null], required: false, default: void 0 },
113
114
  scripting: { type: Boolean, required: false, default: false },
114
115
  menuItems: { type: Array, required: false, default: void 0 },
116
+ selectionItems: { type: Array, required: false, default: void 0 },
115
117
  tools: { type: Array, required: false, default: void 0 },
116
118
  clickToSign: { type: Boolean, required: false, default: false },
117
119
  editablePlacedFields: { type: Boolean, required: false, default: false },
@@ -2,6 +2,7 @@ import { type ComponentPublicInstance } from 'vue';
2
2
  import type { ViewMode } from '../composables/useViewerState.js';
3
3
  import type { FormFieldDefinition, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
4
4
  import type { ViewerMenuItem } from '../menu-items.js';
5
+ import type { ViewerSelectionItem } from '../selection-items.js';
5
6
  import type { ViewerToolEntry } from '../toolbar-tools.js';
6
7
  export interface ViewerTabItem {
7
8
  /** Unique identifier for the tab. */
@@ -63,6 +64,8 @@ type __VLS_Props = {
63
64
  scripting?: boolean;
64
65
  /** Forwarded to every Viewer instance — see KViewer's `menuItems`. */
65
66
  menuItems?: ViewerMenuItem[];
67
+ /** Forwarded to every Viewer instance — see KViewer's `selectionItems`. */
68
+ selectionItems?: ViewerSelectionItem[];
66
69
  /** Forwarded to every Viewer instance — see KViewer's `tools`. */
67
70
  tools?: ViewerToolEntry[];
68
71
  /** Forwarded to every Viewer instance — see KViewer's `clickToSign`. */
@@ -436,6 +439,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
436
439
  activeRoleId: string | null;
437
440
  editablePlacedFields: boolean;
438
441
  fieldDefaults: PlacedFieldDefaults;
442
+ selectionItems: ViewerSelectionItem[];
439
443
  defaultActiveTab: string;
440
444
  minTabs: number;
441
445
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -6,7 +6,10 @@
6
6
  'kviewer-form-field--placed': isPlaced,
7
7
  'kviewer-form-field--selected': props.selected
8
8
  }"
9
- :style="[positionStyle, { '--kvw-field-color': chromeColor }]"
9
+ :style="[positionStyle, {
10
+ '--kvw-field-color': chromeColor,
11
+ '--kvw-handle-zoom': handleZoom
12
+ }]"
10
13
  :data-element-id="props.field.id"
11
14
  >
12
15
  <div
@@ -29,6 +32,11 @@
29
32
  :page-height="props.pageTransform.height"
30
33
  @select="emit('select')"
31
34
  />
35
+
36
+ <PlacedFieldToolbar
37
+ v-if="showChrome && props.selected && !state.readonly.value"
38
+ :field="props.field"
39
+ />
32
40
  </div>
33
41
  </template>
34
42
 
@@ -44,6 +52,7 @@ import FormDropdown from "./FormDropdown.vue";
44
52
  import FormSignatureField from "./FormSignatureField.vue";
45
53
  import FormButton from "./FormButton.vue";
46
54
  import PlacedFieldChrome from "./PlacedFieldChrome.vue";
55
+ import PlacedFieldToolbar from "./PlacedFieldToolbar.vue";
47
56
  const props = defineProps({
48
57
  field: { type: Object, required: true },
49
58
  pageTransform: { type: Object, required: true },
@@ -58,6 +67,10 @@ const isFormEditMode = computed(() => state.formEditMode.value);
58
67
  const showChrome = computed(
59
68
  () => isFormEditMode.value || formFields.editablePlacedFields.value && isPlaced.value
60
69
  );
70
+ const handleZoom = computed(() => {
71
+ const totalZoom = (props.scale || 1) * (state.visualZoom.value || 1);
72
+ return (1 / totalZoom).toFixed(3);
73
+ });
61
74
  const chromeColor = computed(() => {
62
75
  const id = props.field.roleId;
63
76
  if (!id) return "#1677ff";
@@ -77,5 +90,5 @@ const positionStyle = computed(() => {
77
90
  </script>
78
91
 
79
92
  <style>
80
- .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}.kviewer-form-field--editable{outline:1px dashed color-mix(in srgb,var(--kvw-field-color,#1677ff) 45%,transparent);outline-offset:0}.kviewer-form-field--editable.kviewer-form-field--placed{outline-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 75%,transparent)}.kviewer-form-field--editable.kviewer-form-field--selected{outline:none}
93
+ .kviewer-form-field__body{height:100%;width:100%}.kviewer-form-field__body--locked{pointer-events:none}
81
94
  </style>
@@ -6,10 +6,16 @@
6
6
  ref="rootEl"
7
7
  class="kviewer-placed-chrome"
8
8
  :class="{ 'kviewer-placed-chrome--selected': selected }"
9
- :style="{ '--kvw-handle-zoom': handleZoom }"
10
9
  @pointerdown.stop="onChromePointerDown"
11
10
  @click.stop
12
11
  >
12
+ <div
13
+ class="kviewer-placed-chrome__outline"
14
+ :class="{
15
+ 'kviewer-placed-chrome__outline--placed': field.origin === 'placed',
16
+ 'kviewer-placed-chrome__outline--selected': selected
17
+ }"
18
+ />
13
19
  <div
14
20
  v-if="selected"
15
21
  class="kviewer-placed-chrome__grip"
@@ -33,15 +39,9 @@
33
39
  </template>
34
40
 
35
41
  <script setup>
36
- import { computed, ref } from "vue";
42
+ import { ref } from "vue";
37
43
  import { useFormFields } from "../../composables/useFormFields";
38
- import { useViewerState } from "../../composables/useViewerState";
39
44
  const rootEl = ref(null);
40
- const viewerState = useViewerState();
41
- const handleZoom = computed(() => {
42
- const totalZoom = (props.scale || 1) * (viewerState.visualZoom.value || 1);
43
- return Math.max(0.4, 1 / totalZoom).toFixed(3);
44
- });
45
45
  const props = defineProps({
46
46
  field: { type: Object, required: true },
47
47
  selected: { type: Boolean, required: true },
@@ -127,5 +127,5 @@ function onHandlePointerDown(e, name) {
127
127
  </script>
128
128
 
129
129
  <style>
130
- .kviewer-placed-chrome{cursor:move;inset:0;pointer-events:auto;position:absolute;touch-action:none}.kviewer-placed-chrome--selected{outline:1.5px dashed var(--kvw-field-color,#1677ff);outline-offset:0}.kviewer-placed-chrome__grip{align-items:center;background:color-mix(in srgb,var(--kvw-field-color,#1677ff) 15%,transparent);border:1px solid color-mix(in srgb,var(--kvw-field-color,#1677ff) 40%,transparent);border-radius:calc(3px*var(--kvw-handle-zoom, 1));bottom:calc(100% + 3px*var(--kvw-handle-zoom, 1));color:var(--kvw-field-color,#1677ff);cursor:move;display:flex;font-size:calc(9px*var(--kvw-handle-zoom, 1));gap:calc(3px*var(--kvw-handle-zoom, 1));height:calc(16px*var(--kvw-handle-zoom, 1));left:0;line-height:1;overflow:hidden;padding:0 calc(6px*var(--kvw-handle-zoom, 1)) 0 calc(2px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;right:0;touch-action:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kviewer-placed-chrome__grip-dots{color:var(--kvw-field-color,#1677ff);flex:0 0 auto;height:calc(10px*var(--kvw-handle-zoom, 1));width:calc(10px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__grip-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kviewer-placed-chrome__handle{background:#fff;border:1px solid var(--kvw-field-color,#1677ff);border-radius:2px;height:calc(8px*var(--kvw-handle-zoom, 1));pointer-events:auto;position:absolute;touch-action:none;width:calc(8px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle:before{content:"";inset:calc(-8px*var(--kvw-handle-zoom, 1));position:absolute}.kviewer-placed-chrome__handle--n{left:50%;top:calc(-4px*var(--kvw-handle-zoom, 1));transform:translateX(-50%)}.kviewer-placed-chrome__handle--s{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:50%;transform:translateX(-50%)}.kviewer-placed-chrome__handle--e{right:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--w{left:calc(-4px*var(--kvw-handle-zoom, 1));top:50%;transform:translateY(-50%)}.kviewer-placed-chrome__handle--ne{right:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--nw{left:calc(-4px*var(--kvw-handle-zoom, 1));top:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--se{bottom:calc(-4px*var(--kvw-handle-zoom, 1));right:calc(-4px*var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__handle--sw{bottom:calc(-4px*var(--kvw-handle-zoom, 1));left:calc(-4px*var(--kvw-handle-zoom, 1))}
130
+ .kviewer-placed-chrome{cursor:move;inset:0;pointer-events:auto;position:absolute;touch-action:none}.kviewer-placed-chrome__outline{border:1px dashed color-mix(in srgb,var(--kvw-field-color,#1677ff) 45%,transparent);box-sizing:border-box;height:calc(100%/var(--kvw-handle-zoom, 1));left:0;pointer-events:none;position:absolute;top:0;transform:scale(var(--kvw-handle-zoom,1));transform-origin:0 0;width:calc(100%/var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__outline--placed{border-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 75%,transparent)}.kviewer-placed-chrome__outline--selected{border:1.5px dashed var(--kvw-field-color,#1677ff)}.kviewer-placed-chrome__grip{align-items:center;background:color-mix(in srgb,var(--kvw-field-color,#1677ff) 15%,transparent);border:1px solid color-mix(in srgb,var(--kvw-field-color,#1677ff) 40%,transparent);border-radius:4px;bottom:calc(100% + 4px*var(--kvw-handle-zoom, 1));box-sizing:border-box;color:var(--kvw-field-color,#1677ff);cursor:move;display:flex;font-size:12px;gap:4px;height:22px;left:0;line-height:1;overflow:hidden;padding:0 8px 0 3px;pointer-events:auto;position:absolute;touch-action:none;transform:scale(var(--kvw-handle-zoom,1));transform-origin:0 100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:calc(100%/var(--kvw-handle-zoom, 1))}.kviewer-placed-chrome__grip-dots{color:var(--kvw-field-color,#1677ff);flex:0 0 auto;height:13px;width:13px}.kviewer-placed-chrome__grip-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kviewer-placed-chrome__handle{background:#fff;border:1px solid var(--kvw-field-color,#1677ff);border-radius:2px;height:8px;pointer-events:auto;position:absolute;touch-action:none;transform:scale(var(--kvw-handle-zoom,1));width:8px}.kviewer-placed-chrome__handle:before{content:"";inset:-8px;position:absolute}.kviewer-placed-chrome__handle--n{left:50%;top:-4px;transform:translateX(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--s{bottom:-4px;left:50%;transform:translateX(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--e{right:-4px;top:50%;transform:translateY(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--w{left:-4px;top:50%;transform:translateY(-50%) scale(var(--kvw-handle-zoom,1))}.kviewer-placed-chrome__handle--ne{right:-4px;top:-4px}.kviewer-placed-chrome__handle--nw{left:-4px;top:-4px}.kviewer-placed-chrome__handle--se{bottom:-4px;right:-4px}.kviewer-placed-chrome__handle--sw{bottom:-4px;left:-4px}
131
131
  </style>
@@ -0,0 +1,7 @@
1
+ import type { FormFieldDefinition } from '../../annotation/engine/types.js';
2
+ type __VLS_Props = {
3
+ field: FormFieldDefinition;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <!-- Same pill as AnnotationToolbar, but anchored in the page DOM below
3
+ the selected field so it follows pan/zoom for free (the Konva
4
+ selection popover is viewport-fixed because the stage has no DOM
5
+ anchor to attach to). Stops pointer events so a click here never
6
+ reaches page-level deselect handlers. -->
7
+ <div
8
+ class="kviewer-placed-toolbar"
9
+ @pointerdown.stop
10
+ @click.stop
11
+ >
12
+ <div class="flex items-center gap-0.5 bg-elevated rounded-lg shadow-lg border border-default p-1">
13
+ <SelectionToolbarItems :items="customItems" :ctx="selectionCtx" />
14
+
15
+ <button
16
+ class="flex items-center justify-center w-7 h-7 rounded-md hover:bg-default cursor-pointer"
17
+ :title="t('deleteField')"
18
+ data-testid="placed-field-delete"
19
+ @click="onDelete"
20
+ >
21
+ <UIcon name="i-lucide-trash-2" class="text-sm text-error" />
22
+ </button>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { computed, inject } from "vue";
29
+ import { useFormFields } from "../../composables/useFormFields";
30
+ import { useKviewerI18n } from "../../composables/useKviewerI18n";
31
+ import { SELECTION_ITEMS_KEY } from "../../selection-items";
32
+ import SelectionToolbarItems from "../SelectionToolbarItems.vue";
33
+ const props = defineProps({
34
+ field: { type: Object, required: true }
35
+ });
36
+ const formFields = useFormFields();
37
+ const { t } = useKviewerI18n();
38
+ const customItems = inject(SELECTION_ITEMS_KEY, void 0);
39
+ const selectionCtx = computed(() => ({
40
+ kind: "field",
41
+ annotations: [],
42
+ field: props.field
43
+ }));
44
+ function onDelete() {
45
+ formFields.removePlacedField(props.field.id);
46
+ }
47
+ </script>
48
+
49
+ <style>
50
+ .kviewer-placed-toolbar{left:50%;pointer-events:auto;position:absolute;top:calc(100% + 8px*var(--kvw-handle-zoom, 1));transform:translateX(-50%) scale(var(--kvw-handle-zoom,1));transform-origin:50% 0;width:-moz-max-content;width:max-content;z-index:1}
51
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { FormFieldDefinition } from '../../annotation/engine/types.js';
2
+ type __VLS_Props = {
3
+ field: FormFieldDefinition;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -13,6 +13,16 @@
13
13
  data-testid="viewer-download"
14
14
  @click="onDownload"
15
15
  />
16
+ <UButton
17
+ icon="i-lucide-printer"
18
+ :label="t('print')"
19
+ variant="ghost"
20
+ color="neutral"
21
+ size="xs"
22
+ class="w-full justify-start"
23
+ data-testid="viewer-print"
24
+ @click="onPrint"
25
+ />
16
26
  <UButton
17
27
  :icon="state.shapeDetection.value ? 'i-lucide-square-check-big' : 'i-lucide-square-dashed'"
18
28
  :label="state.shapeDetection.value ? t('disableFormFieldDetection') : t('detectFormFields')"
@@ -76,4 +86,11 @@ async function onDownload() {
76
86
  console.error("Failed to export PDF from menu action", error);
77
87
  }
78
88
  }
89
+ async function onPrint() {
90
+ try {
91
+ await state.printPdf();
92
+ } catch (error) {
93
+ console.error("Failed to print PDF from menu action", error);
94
+ }
95
+ }
79
96
  </script>
@@ -44,6 +44,11 @@ export interface ScriptingManager {
44
44
  * PageOpen action can fire (the manager's `updatefromsandbox` /
45
45
  * pagerendered handling in pdf_viewer.mjs). */
46
46
  notifyPageRendered: (pageNumber: number) => void;
47
+ /** Run the document's `WillPrint` action and wait for the sandbox to
48
+ * finish it (scripts may adjust field values before printing). */
49
+ dispatchWillPrint: () => Promise<void>;
50
+ /** Fire the document's `DidPrint` action. */
51
+ dispatchDidPrint: () => Promise<void>;
47
52
  /** Bind a document to the sandbox. Safe to call repeatedly — pass
48
53
  * null to detach (e.g. before swapping documents). */
49
54
  setDocument: (pdfDoc: PDFDocumentProxy | null) => Promise<void>;
@@ -104,6 +104,12 @@ export async function createScriptingManager(deps) {
104
104
  notifyPageRendered(pageNumber) {
105
105
  eventBus.dispatch("pagerendered", { source: window, pageNumber });
106
106
  },
107
+ async dispatchWillPrint() {
108
+ await manager.dispatchWillPrint();
109
+ },
110
+ async dispatchDidPrint() {
111
+ await manager.dispatchDidPrint();
112
+ },
107
113
  async setDocument(pdfDoc) {
108
114
  await manager.setDocument(pdfDoc);
109
115
  },
@@ -54,6 +54,7 @@ export interface ViewerState {
54
54
  setStylusMode: (enabled: boolean) => void;
55
55
  scrollToPage: (pageNumber: number) => void;
56
56
  downloadPdf: () => Promise<void>;
57
+ printPdf: () => Promise<void>;
57
58
  history: AnnotationHistory;
58
59
  readonly: Ref<boolean>;
59
60
  shapeDetection: Ref<boolean>;
@@ -66,6 +67,7 @@ export interface ViewerStateProvider {
66
67
  state: ViewerState;
67
68
  setScrollToPageFn: (fn: (pageNumber: number) => void) => void;
68
69
  setDownloadPdfFn: (fn: () => Promise<void>) => void;
70
+ setPrintPdfFn: (fn: () => Promise<void>) => void;
69
71
  /** Register how custom-scale changes are applied. The Viewer registers a
70
72
  * position-preserving commit (zoom about the viewport centre); without
71
73
  * one, setScale falls back to a plain scale write. */
@@ -152,12 +152,16 @@ export function provideViewerState() {
152
152
  }
153
153
  let _scrollToPageFn = null;
154
154
  let _downloadPdfFn = null;
155
+ let _printPdfFn = null;
155
156
  function scrollToPage(pageNumber) {
156
157
  _scrollToPageFn?.(pageNumber);
157
158
  }
158
159
  async function downloadPdf() {
159
160
  await _downloadPdfFn?.();
160
161
  }
162
+ async function printPdf() {
163
+ await _printPdfFn?.();
164
+ }
161
165
  const history = createAnnotationHistory(annotations, painter);
162
166
  const state = {
163
167
  scale,
@@ -200,6 +204,7 @@ export function provideViewerState() {
200
204
  setStylusMode,
201
205
  scrollToPage,
202
206
  downloadPdf,
207
+ printPdf,
203
208
  history,
204
209
  readonly,
205
210
  shapeDetection: shapeDetectionEnabled,
@@ -217,6 +222,9 @@ export function provideViewerState() {
217
222
  setDownloadPdfFn(fn) {
218
223
  _downloadPdfFn = fn;
219
224
  },
225
+ setPrintPdfFn(fn) {
226
+ _printPdfFn = fn;
227
+ },
220
228
  setApplyScaleFn(fn) {
221
229
  _applyScaleFn = fn;
222
230
  }
@@ -44,6 +44,10 @@ export declare class KViewerEmbedClient {
44
44
  skipped: number;
45
45
  }>;
46
46
  exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
47
+ /** Open the viewer's print dialog on the exported (vector) PDF —
48
+ * annotations and form values included, at full print quality.
49
+ * Resolves once the dialog is up, not when printing finishes. */
50
+ printPdf(options?: Omit<ExportPdfOptions, 'download' | 'fileName'>): Promise<void>;
47
51
  getFormFieldValues(): Promise<FormFieldValue[]>;
48
52
  setFormFieldValue(fieldName: string, value: string | boolean | string[]): Promise<void>;
49
53
  addFormField(payload: AddFormFieldPayload): Promise<FormFieldDefinition>;
@@ -60,6 +60,12 @@ export class KViewerEmbedClient {
60
60
  exportPdf(options) {
61
61
  return this.call("exportPdf", options);
62
62
  }
63
+ /** Open the viewer's print dialog on the exported (vector) PDF —
64
+ * annotations and form values included, at full print quality.
65
+ * Resolves once the dialog is up, not when printing finishes. */
66
+ printPdf(options) {
67
+ return this.call("printPdf", options);
68
+ }
63
69
  getFormFieldValues() {
64
70
  return this.call("getFormFieldValues");
65
71
  }
@@ -17,6 +17,7 @@ export interface KViewerApi {
17
17
  skipped: number;
18
18
  }>;
19
19
  exportPdf: (options?: ExportPdfOptions) => Promise<Uint8Array>;
20
+ printPdf: (options?: ExportPdfOptions) => Promise<void>;
20
21
  getFormFieldValues: () => FormFieldValue[];
21
22
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
22
23
  addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
@@ -39,6 +40,14 @@ export interface KViewerApi {
39
40
  getViewedPages: () => number[];
40
41
  resetViewedPages: () => void;
41
42
  }
43
+ /**
44
+ * What the bridge actually requires of a host. Every capability is
45
+ * optional so an embedding application can hand over an adapter exposing
46
+ * only the parts it uses — a real `<KViewer>` ref satisfies this too.
47
+ * Unimplemented methods surface as an `UNSUPPORTED_METHOD_ERROR_NAME`
48
+ * response instead of breaking initialization.
49
+ */
50
+ export type KViewerHostApi = Partial<KViewerApi>;
42
51
  export interface KViewerEmbedBridgeOptions {
43
52
  /**
44
53
  * Origin of the parent window that hosts this iframe. Used both as
@@ -67,5 +76,5 @@ interface BridgeHandle {
67
76
  /** Manually tear down listeners. Called automatically on scope dispose. */
68
77
  unmount: () => void;
69
78
  }
70
- export declare function useKViewerEmbedBridge(viewerRef: Ref<KViewerApi | null | undefined>, options: KViewerEmbedBridgeOptions): BridgeHandle;
79
+ export declare function useKViewerEmbedBridge(viewerRef: Ref<KViewerHostApi | null | undefined>, options: KViewerEmbedBridgeOptions): BridgeHandle;
71
80
  export type { MethodName };