kviewer 0.3.5 → 0.3.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.3.5",
4
+ "version": "0.3.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -1,4 +1,5 @@
1
1
  import { type ViewMode } from '../composables/useViewerState.js';
2
+ import type { ScrollBoundaryPayload } from '../embed/protocol.js';
2
3
  import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
4
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
5
  import type { ViewerMenuItem } from '../menu-items.js';
@@ -154,6 +155,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
154
155
  allSigned: boolean;
155
156
  fields: SignatureFieldStatus[];
156
157
  }) => void) => (() => void);
158
+ /** Subscribe to vertical wheel movement left over when the viewer reaches
159
+ * a document boundary. Used by the embed bridge for parent scrolling. */
160
+ onScrollBoundary: (cb: (payload: ScrollBoundaryPayload) => void) => (() => void);
157
161
  /** Reactive form-edit-mode flag — read with `.value`. */
158
162
  formEditMode: import("vue").Ref<boolean, boolean>;
159
163
  /** Programmatically enter or leave form-edit mode. */
@@ -263,6 +263,7 @@ const fieldPlacedCallbacks = /* @__PURE__ */ new Set();
263
263
  const signedStatusCallbacks = /* @__PURE__ */ new Set();
264
264
  const fieldUpdatedCallbacks = /* @__PURE__ */ new Set();
265
265
  const fieldRemovedCallbacks = /* @__PURE__ */ new Set();
266
+ const scrollBoundaryCallbacks = /* @__PURE__ */ new Set();
266
267
  const viewerRoot = ref(null);
267
268
  const scrollContainer = ref(null);
268
269
  const { state: viewerState, setScrollToPageFn, setDownloadPdfFn, setPrintPdfFn, setApplyScaleFn } = provideViewerState();
@@ -864,28 +865,43 @@ function initPanzoom() {
864
865
  inertiaPanzoom.attach(zoomWrapper.value);
865
866
  }
866
867
  let wheelCooldown = false;
868
+ function notifyScrollBoundary(deltaY) {
869
+ if (deltaY === 0) return;
870
+ const payload = {
871
+ edge: deltaY > 0 ? "end" : "start",
872
+ deltaY
873
+ };
874
+ for (const callback of scrollBoundaryCallbacks) callback(payload);
875
+ }
867
876
  function onWheel(event) {
868
877
  if (event.ctrlKey || event.metaKey) return;
869
878
  if (isSinglePageMode.value) {
870
879
  event.preventDefault();
871
- if (wheelCooldown) return;
880
+ const total = viewerState.totalPages.value;
881
+ const cur = viewerState.currentPage.value;
882
+ const isAtBoundary = event.deltaY > 0 && cur >= total || event.deltaY < 0 && cur <= 1;
883
+ if (wheelCooldown) {
884
+ if (isAtBoundary) notifyScrollBoundary(event.deltaY);
885
+ return;
886
+ }
872
887
  wheelCooldown = true;
873
888
  setTimeout(() => {
874
889
  wheelCooldown = false;
875
890
  }, 300);
876
- const total = viewerState.totalPages.value;
877
- const cur = viewerState.currentPage.value;
878
891
  if (event.deltaY > 0 && cur < total) {
879
892
  viewerState.currentPage.value = cur + 1;
880
893
  } else if (event.deltaY < 0 && cur > 1) {
881
894
  viewerState.currentPage.value = cur - 1;
895
+ } else {
896
+ notifyScrollBoundary(event.deltaY);
882
897
  }
883
898
  return;
884
899
  }
885
900
  event.preventDefault();
886
901
  const dx = event.shiftKey ? event.deltaY : event.deltaX;
887
902
  const dy = event.shiftKey ? 0 : event.deltaY;
888
- inertiaPanzoom.scrollBy(dx, dy);
903
+ const remaining = inertiaPanzoom.scrollBy(dx, dy);
904
+ notifyScrollBoundary(remaining.y);
889
905
  }
890
906
  function isTextInputTarget(target) {
891
907
  if (!(target instanceof HTMLElement)) return false;
@@ -1181,6 +1197,12 @@ defineExpose({
1181
1197
  signedStatusCallbacks.add(cb);
1182
1198
  return () => signedStatusCallbacks.delete(cb);
1183
1199
  },
1200
+ /** Subscribe to vertical wheel movement left over when the viewer reaches
1201
+ * a document boundary. Used by the embed bridge for parent scrolling. */
1202
+ onScrollBoundary: (cb) => {
1203
+ scrollBoundaryCallbacks.add(cb);
1204
+ return () => scrollBoundaryCallbacks.delete(cb);
1205
+ },
1184
1206
  /** Reactive form-edit-mode flag — read with `.value`. */
1185
1207
  formEditMode: viewerState.formEditMode,
1186
1208
  /** Programmatically enter or leave form-edit mode. */
@@ -1,4 +1,5 @@
1
1
  import { type ViewMode } from '../composables/useViewerState.js';
2
+ import type { ScrollBoundaryPayload } from '../embed/protocol.js';
2
3
  import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, PlacedFieldDefaults, SignatureFieldStatus, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
4
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
5
  import type { ViewerMenuItem } from '../menu-items.js';
@@ -154,6 +155,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
154
155
  allSigned: boolean;
155
156
  fields: SignatureFieldStatus[];
156
157
  }) => void) => (() => void);
158
+ /** Subscribe to vertical wheel movement left over when the viewer reaches
159
+ * a document boundary. Used by the embed bridge for parent scrolling. */
160
+ onScrollBoundary: (cb: (payload: ScrollBoundaryPayload) => void) => (() => void);
157
161
  /** Reactive form-edit-mode flag — read with `.value`. */
158
162
  formEditMode: import("vue").Ref<boolean, boolean>;
159
163
  /** Programmatically enter or leave form-edit mode. */
@@ -17,6 +17,11 @@ export interface Transform {
17
17
  tx: number;
18
18
  ty: number;
19
19
  }
20
+ export interface ScrollDelta {
21
+ x: number;
22
+ y: number;
23
+ }
24
+ export declare function getUnconsumedScrollDelta(requested: ScrollDelta, before: Transform, after: Transform): ScrollDelta;
20
25
  export interface InertiaPanzoomHandle {
21
26
  /** Bind gesture handlers to the given element (its parent becomes the viewport). */
22
27
  attach: (target: HTMLElement) => void;
@@ -30,8 +35,9 @@ export interface InertiaPanzoomHandle {
30
35
  getTransform: () => Transform;
31
36
  /** Animate the view so `elem` is at the top (or center) of the viewport. */
32
37
  scrollToElement: (elem: HTMLElement, opts?: ScrollToElementOptions) => void;
33
- /** Pan by a delta in viewport pixels (clamped to bounds). */
34
- scrollBy: (dx: number, dy: number) => void;
38
+ /** Pan by a delta in viewport pixels and return the portion that could not
39
+ * be consumed because the viewport reached a boundary. */
40
+ scrollBy: (dx: number, dy: number) => ScrollDelta;
35
41
  /** Enable/disable pan+pinch gestures (wheel/scrollbar/keyboard still work). */
36
42
  setGesturesEnabled: (enabled: boolean) => void;
37
43
  /** Subscribe to transform updates. Returns an unsubscribe function. */
@@ -1,14 +1,17 @@
1
1
  import { onBeforeUnmount } from "vue";
2
+ export function getUnconsumedScrollDelta(requested, before, after) {
3
+ return {
4
+ x: requested.x - (before.tx - after.tx),
5
+ y: requested.y - (before.ty - after.ty)
6
+ };
7
+ }
2
8
  const DECELERATION = 0.998;
3
9
  const RUBBER_BAND_C = 0.55;
4
10
  const BOUNCE_BACK_RATE = 0.04;
5
11
  const BOUNCE_VELOCITY_DECAY = 0.04;
6
12
  const MIN_VELOCITY = 0.01;
7
13
  export function useInertiaPanzoom(options = {}) {
8
- const {
9
- axisLockThreshold = 10,
10
- axisLockRatio = 2
11
- } = options;
14
+ const { axisLockThreshold = 10, axisLockRatio = 2 } = options;
12
15
  let minScale = options.minScale ?? 0.5;
13
16
  let maxScale = options.maxScale ?? 4;
14
17
  const subscribers = /* @__PURE__ */ new Set();
@@ -139,11 +142,17 @@ export function useInertiaPanzoom(options = {}) {
139
142
  if (barDrag.axis === "y") {
140
143
  const dy = e.clientY - barDrag.startPointer;
141
144
  const b = getBounds();
142
- ty = Math.max(b.minY, Math.min(b.maxY, barDrag.startT - dy * barDrag.ratio));
145
+ ty = Math.max(
146
+ b.minY,
147
+ Math.min(b.maxY, barDrag.startT - dy * barDrag.ratio)
148
+ );
143
149
  } else {
144
150
  const dx = e.clientX - barDrag.startPointer;
145
151
  const b = getBounds();
146
- tx = Math.max(b.minX, Math.min(b.maxX, barDrag.startT - dx * barDrag.ratio));
152
+ tx = Math.max(
153
+ b.minX,
154
+ Math.min(b.maxX, barDrag.startT - dx * barDrag.ratio)
155
+ );
147
156
  }
148
157
  apply();
149
158
  }
@@ -226,8 +235,10 @@ export function useInertiaPanzoom(options = {}) {
226
235
  return { minX, maxX, minY, maxY };
227
236
  }
228
237
  function rubberBandAxis(v, min, max, dim) {
229
- if (v < min) return min - (1 - 1 / ((min - v) * RUBBER_BAND_C / dim + 1)) * dim;
230
- if (v > max) return max + (1 - 1 / ((v - max) * RUBBER_BAND_C / dim + 1)) * dim;
238
+ if (v < min)
239
+ return min - (1 - 1 / ((min - v) * RUBBER_BAND_C / dim + 1)) * dim;
240
+ if (v > max)
241
+ return max + (1 - 1 / ((v - max) * RUBBER_BAND_C / dim + 1)) * dim;
231
242
  return v;
232
243
  }
233
244
  function stopMomentum() {
@@ -320,7 +331,10 @@ export function useInertiaPanzoom(options = {}) {
320
331
  const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
321
332
  const midX = (pts[0].x + pts[1].x) / 2;
322
333
  const midY = (pts[0].y + pts[1].y) / 2;
323
- const newScale = Math.max(minScale, Math.min(maxScale, pinchStart.s * (dist / pinchStart.dist)));
334
+ const newScale = Math.max(
335
+ minScale,
336
+ Math.min(maxScale, pinchStart.s * (dist / pinchStart.dist))
337
+ );
324
338
  const rect = parent.getBoundingClientRect();
325
339
  const focalX = (pinchStart.midX - rect.left - pinchStart.tx) / pinchStart.s;
326
340
  const focalY = (pinchStart.midY - rect.top - pinchStart.ty) / pinchStart.s;
@@ -386,7 +400,10 @@ export function useInertiaPanzoom(options = {}) {
386
400
  const mx = e.clientX - rect.left;
387
401
  const my = e.clientY - rect.top;
388
402
  const delta = e.deltaY < 0 ? 1 : -1;
389
- const newScale = Math.max(minScale, Math.min(maxScale, scale * Math.exp(delta * 0.1)));
403
+ const newScale = Math.max(
404
+ minScale,
405
+ Math.min(maxScale, scale * Math.exp(delta * 0.1))
406
+ );
390
407
  const focalX = (mx - tx) / scale;
391
408
  const focalY = (my - ty) / scale;
392
409
  tx = mx - newScale * focalX;
@@ -398,7 +415,8 @@ export function useInertiaPanzoom(options = {}) {
398
415
  }
399
416
  function setTransform(t) {
400
417
  stopMomentum();
401
- if (t.scale !== void 0) scale = Math.max(minScale, Math.min(maxScale, t.scale));
418
+ if (t.scale !== void 0)
419
+ scale = Math.max(minScale, Math.min(maxScale, t.scale));
402
420
  if (t.tx !== void 0) tx = t.tx;
403
421
  if (t.ty !== void 0) ty = t.ty;
404
422
  const b = getBounds();
@@ -424,9 +442,15 @@ export function useInertiaPanzoom(options = {}) {
424
442
  function scrollBy(dx, dy) {
425
443
  stopMomentum();
426
444
  const b = getBounds();
445
+ const before = { scale, tx, ty };
427
446
  tx = Math.max(b.minX, Math.min(b.maxX, tx - dx));
428
447
  ty = Math.max(b.minY, Math.min(b.maxY, ty - dy));
429
448
  apply();
449
+ return getUnconsumedScrollDelta({ x: dx, y: dy }, before, {
450
+ scale,
451
+ tx,
452
+ ty
453
+ });
430
454
  }
431
455
  parent.addEventListener("pointerdown", onDown);
432
456
  parent.addEventListener("pointermove", onMove);
@@ -504,7 +528,7 @@ export function useInertiaPanzoom(options = {}) {
504
528
  getScale: () => controller?.getScale() ?? 1,
505
529
  getTransform: () => controller?.getTransform() ?? { scale: 1, tx: 0, ty: 0 },
506
530
  scrollToElement: (elem, opts) => controller?.scrollToElement(elem, opts),
507
- scrollBy: (dx, dy) => controller?.scrollBy(dx, dy),
531
+ scrollBy: (dx, dy) => controller?.scrollBy(dx, dy) ?? { x: dx, y: dy },
508
532
  setGesturesEnabled: (enabled) => controller?.setGesturesEnabled(enabled),
509
533
  setTransform: (t) => controller?.setTransform(t),
510
534
  setScaleLimits: (min, max) => {
@@ -22,7 +22,7 @@ type EventHandler<E extends EventName> = (payload: EventPayloads[E]) => void;
22
22
  * viewer's exposed methods, and surfaces iframe-emitted events
23
23
  * (`ready`, `formEditMode-changed`, `viewedPages-changed`, `all-pages-read`,
24
24
  * `field-placed`, `field-updated`, `field-removed`, `signedStatus-changed`)
25
- * via `on()`.
25
+ * and `scroll-boundary`) via `on()`.
26
26
  */
27
27
  export declare class KViewerEmbedClient {
28
28
  private readonly iframe;
@@ -1,7 +1,7 @@
1
1
  import { type Ref } from 'vue';
2
2
  import type { ExportPdfOptions } from '../annotation/pdf-export/export.js';
3
3
  import type { AddFormFieldPayload, FormFieldDefinition, FormFieldValue, IAnnotationStore, PlacedFieldDefaults, SignatureFieldStatus } from '../annotation/engine/types.js';
4
- import { type MethodName } from './protocol.js';
4
+ import { type MethodName, type ScrollBoundaryPayload } from './protocol.js';
5
5
  /**
6
6
  * Surface the bridge calls on the KViewer template ref. Mirrors the
7
7
  * methods defined in `defineExpose` inside Viewer.vue. Kept as a
@@ -35,6 +35,7 @@ export interface KViewerApi {
35
35
  allSigned: boolean;
36
36
  fields: SignatureFieldStatus[];
37
37
  }) => void) => () => void;
38
+ onScrollBoundary: (cb: (payload: ScrollBoundaryPayload) => void) => () => void;
38
39
  formEditMode: boolean | Ref<boolean>;
39
40
  setFormEditMode: (enabled: boolean) => void;
40
41
  toggleFormEditMode: () => boolean;
@@ -226,6 +226,14 @@ export function useKViewerEmbedBridge(viewerRef, options) {
226
226
  )
227
227
  );
228
228
  }
229
+ if (typeof viewer.onScrollBoundary === "function") {
230
+ events.push("scroll-boundary");
231
+ stopViewerStateWatches.push(
232
+ viewer.onScrollBoundary(
233
+ (payload) => postEvent("scroll-boundary", payload)
234
+ )
235
+ );
236
+ }
229
237
  if (typeof viewer.allPagesRead === "function") {
230
238
  events.push("all-pages-read");
231
239
  stopViewerStateWatches.push(
@@ -11,6 +11,10 @@ export declare const EVENT_KIND: "kviewer:event";
11
11
  */
12
12
  export declare const UNSUPPORTED_METHOD_ERROR_NAME = "KViewerUnsupportedMethodError";
13
13
  export type ImportAnnotationsMode = 'replace' | 'merge';
14
+ export interface ScrollBoundaryPayload {
15
+ edge: 'start' | 'end';
16
+ deltaY: number;
17
+ }
14
18
  export interface MethodSignatures {
15
19
  getAnnotations: {
16
20
  args: [];
@@ -145,6 +149,9 @@ export interface EventPayloads {
145
149
  /** Fires once when the final unseen page is viewed. Re-arms after
146
150
  * `resetViewedPages()` or a new document. */
147
151
  'all-pages-read': Record<string, never>;
152
+ /** Vertical wheel movement that the embedded viewer could not consume
153
+ * because it reached the start or end of the document. */
154
+ 'scroll-boundary': ScrollBoundaryPayload;
148
155
  /** The user placed a form field via a placement tool. Patch it with
149
156
  * `updateFormField()` to set host-driven properties. */
150
157
  'field-placed': {
@@ -7,7 +7,7 @@ export type { KViewerApi, KViewerHostApi, KViewerEmbedBridgeOptions, } from './e
7
7
  export { useKViewerEmbedBridge } from './embed/bridge-host.js';
8
8
  export type { KViewerEmbedClientOptions } from './embed/bridge-client.js';
9
9
  export { KViewerEmbedClient } from './embed/bridge-client.js';
10
- export type { EventName as KViewerEmbedEventName, EventPayloads as KViewerEmbedEventPayloads, MethodName as KViewerEmbedMethodName, } from './embed/protocol.js';
10
+ export type { EventName as KViewerEmbedEventName, EventPayloads as KViewerEmbedEventPayloads, MethodName as KViewerEmbedMethodName, ScrollBoundaryPayload, } from './embed/protocol.js';
11
11
  export { KVIEWER_EMBED_PROTOCOL_VERSION, UNSUPPORTED_METHOD_ERROR_NAME as KVIEWER_EMBED_UNSUPPORTED_METHOD_ERROR_NAME, } from './embed/protocol.js';
12
12
  export type { ViewerMenuItem, ViewerMenuButtonItem, ViewerMenuCheckboxItem, ViewerMenuSeparatorItem, } from './menu-items.js';
13
13
  export type { ViewerSelectionItem, ViewerSelectionButtonItem, ViewerSelectionSelectItem, ViewerSelectionContext, } from './selection-items.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kviewer",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Kabema PDF Editor",
5
5
  "repository": "kabema/kviewer",
6
6
  "license": "MIT",