pptx-angular-viewer 1.1.35 → 1.1.37

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project are documented here.
4
4
  This file is generated from [Conventional Commits](https://www.conventionalcommits.org)
5
5
  by [git-cliff](https://git-cliff.org); do not edit it by hand.
6
6
 
7
+ ## [1.1.35](https://github.com/ChristopherVR/pptx-viewer/releases/tag/pptx-angular-viewer@1.1.35) - 2026-06-21
8
+
9
+ ### Features
10
+
11
+ - **angular:** Add full SmartArt editing inspector (by @ChristopherVR) ([c7ab8e2](https://github.com/ChristopherVR/pptx-viewer/commit/c7ab8e24ff3965dae0d18cc9f7373bfc510a62c4))
12
+
7
13
  ## [1.1.32](https://github.com/ChristopherVR/pptx-viewer/releases/tag/pptx-angular-viewer@1.1.32) - 2026-06-21
8
14
 
9
15
  ### Dependencies
@@ -25424,6 +25424,149 @@ function toggleCommentResolvedInList(comments, id) {
25424
25424
  return next;
25425
25425
  }
25426
25426
 
25427
+ /**
25428
+ * Framework-agnostic touch-gesture state machine for the viewer canvas.
25429
+ *
25430
+ * Recognises three gestures from raw DOM `TouchEvent`s and emits callbacks:
25431
+ * - Pinch-to-zoom: two-finger spread/pinch, scaled by the distance ratio.
25432
+ * - Swipe: single-finger horizontal swipe (slide navigation).
25433
+ * - Long-press: single-finger hold (context-menu trigger).
25434
+ *
25435
+ * The recogniser holds all mutable pinch/swipe/long-press state internally and
25436
+ * exposes four event handlers plus a `cancel()`. The listener attach/detach
25437
+ * lifecycle (and any framework reactivity) stays in each binding; only this
25438
+ * pure state machine is shared.
25439
+ *
25440
+ * @module touch-gestures
25441
+ */
25442
+ // ---------------------------------------------------------------------------
25443
+ // Constants
25444
+ // ---------------------------------------------------------------------------
25445
+ /** The minimum horizontal distance (px) to recognise a swipe. */
25446
+ const SWIPE_THRESHOLD_PX = 50;
25447
+ /** Maximum vertical deviation (px) for a swipe to still count. */
25448
+ const SWIPE_MAX_VERTICAL_PX = 100;
25449
+ /** The hold duration (ms) for a long-press. */
25450
+ const LONG_PRESS_DURATION_MS = 500;
25451
+ /** If the finger moves more than this (px) during a hold, cancel the long-press. */
25452
+ const LONG_PRESS_MOVE_TOLERANCE_PX = 10;
25453
+ // ---------------------------------------------------------------------------
25454
+ // Pure helpers
25455
+ // ---------------------------------------------------------------------------
25456
+ /** Compute the Euclidean distance between two touch points. */
25457
+ function getTouchDistance(t1, t2) {
25458
+ const dx = t1.clientX - t2.clientX;
25459
+ const dy = t1.clientY - t2.clientY;
25460
+ return Math.sqrt(dx * dx + dy * dy);
25461
+ }
25462
+ /**
25463
+ * Clamp a scale value into the allowed zoom range. The bounds are parameters
25464
+ * (not imported constants) to keep this module framework- and binding-agnostic.
25465
+ */
25466
+ function clampScale$1(value, minScale, maxScale) {
25467
+ return Math.min(Math.max(value, minScale), maxScale);
25468
+ }
25469
+ // ---------------------------------------------------------------------------
25470
+ // Recogniser factory
25471
+ // ---------------------------------------------------------------------------
25472
+ /**
25473
+ * Create a stateful touch-gesture recogniser. The returned handlers mirror the
25474
+ * React viewer's native `touch*` listeners exactly, including `preventDefault()`
25475
+ * on the pinch path and the `setTimeout`-based long-press timer.
25476
+ */
25477
+ function createTouchGestureRecognizer(config) {
25478
+ const { getScale, minScale, maxScale, callbacks } = config;
25479
+ // Pinch state.
25480
+ let initialPinchDistance = 0;
25481
+ let pinchBaseScale = 1;
25482
+ let isPinching = false;
25483
+ // Swipe state.
25484
+ let swipeStartX = 0;
25485
+ let swipeStartY = 0;
25486
+ // Long-press state.
25487
+ let longPressTimer = null;
25488
+ let longPressStartX = 0;
25489
+ let longPressStartY = 0;
25490
+ const cancelLongPress = () => {
25491
+ if (longPressTimer !== null) {
25492
+ clearTimeout(longPressTimer);
25493
+ longPressTimer = null;
25494
+ }
25495
+ };
25496
+ const onTouchStart = (e) => {
25497
+ if (e.touches.length === 2) {
25498
+ // Start pinch.
25499
+ isPinching = true;
25500
+ initialPinchDistance = getTouchDistance(e.touches[0], e.touches[1]);
25501
+ pinchBaseScale = getScale();
25502
+ cancelLongPress();
25503
+ e.preventDefault();
25504
+ }
25505
+ else if (e.touches.length === 1) {
25506
+ // Potential swipe or long-press.
25507
+ swipeStartX = e.touches[0].clientX;
25508
+ swipeStartY = e.touches[0].clientY;
25509
+ longPressStartX = e.touches[0].clientX;
25510
+ longPressStartY = e.touches[0].clientY;
25511
+ cancelLongPress();
25512
+ longPressTimer = setTimeout(() => {
25513
+ longPressTimer = null;
25514
+ callbacks.onLongPress?.(longPressStartX, longPressStartY);
25515
+ }, LONG_PRESS_DURATION_MS);
25516
+ }
25517
+ };
25518
+ const onTouchMove = (e) => {
25519
+ if (e.touches.length === 2 && isPinching) {
25520
+ e.preventDefault();
25521
+ const currentDistance = getTouchDistance(e.touches[0], e.touches[1]);
25522
+ if (initialPinchDistance > 0) {
25523
+ const ratio = currentDistance / initialPinchDistance;
25524
+ const newScale = clampScale$1(pinchBaseScale * ratio, minScale, maxScale);
25525
+ callbacks.onPinchZoom?.(newScale);
25526
+ }
25527
+ }
25528
+ else if (e.touches.length === 1) {
25529
+ // Cancel the long-press if the finger moved too far.
25530
+ const dx = e.touches[0].clientX - longPressStartX;
25531
+ const dy = e.touches[0].clientY - longPressStartY;
25532
+ if (Math.abs(dx) > LONG_PRESS_MOVE_TOLERANCE_PX ||
25533
+ Math.abs(dy) > LONG_PRESS_MOVE_TOLERANCE_PX) {
25534
+ cancelLongPress();
25535
+ }
25536
+ }
25537
+ };
25538
+ const onTouchEnd = (e) => {
25539
+ if (isPinching) {
25540
+ isPinching = false;
25541
+ initialPinchDistance = 0;
25542
+ return;
25543
+ }
25544
+ cancelLongPress();
25545
+ // Detect a swipe from the touch that just ended.
25546
+ if (e.changedTouches.length === 1 && e.touches.length === 0) {
25547
+ const endX = e.changedTouches[0].clientX;
25548
+ const endY = e.changedTouches[0].clientY;
25549
+ const deltaX = endX - swipeStartX;
25550
+ const deltaY = endY - swipeStartY;
25551
+ if (Math.abs(deltaX) >= SWIPE_THRESHOLD_PX && Math.abs(deltaY) < SWIPE_MAX_VERTICAL_PX) {
25552
+ callbacks.onSwipe?.(deltaX > 0 ? 1 : -1);
25553
+ }
25554
+ }
25555
+ };
25556
+ const onTouchCancel = () => {
25557
+ isPinching = false;
25558
+ initialPinchDistance = 0;
25559
+ cancelLongPress();
25560
+ };
25561
+ return {
25562
+ onTouchStart,
25563
+ onTouchMove,
25564
+ onTouchEnd,
25565
+ onTouchCancel,
25566
+ cancel: onTouchCancel,
25567
+ };
25568
+ }
25569
+
25427
25570
  /**
25428
25571
  * insert-chart.ts - framework-agnostic factory for a sensible DEFAULT new
25429
25572
  * chart element, the single source of truth every binding (React, Vue,
@@ -54377,6 +54520,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
54377
54520
  }]
54378
54521
  }], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], activeName: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeName", required: false }] }], applyTheme: [{ type: i0.Output, args: ["applyTheme"] }], close: [{ type: i0.Output, args: ["close"] }] } });
54379
54522
 
54523
+ /**
54524
+ * touch-gestures.ts: thin Angular adapter over the framework-agnostic
54525
+ * `createTouchGestureRecognizer` from `pptx-viewer-shared`.
54526
+ *
54527
+ * The gesture state machine itself (pinch-to-zoom, swipe, long-press) lives in
54528
+ * shared and is identical across React / Vue / Angular. This adapter only owns
54529
+ * the DOM listener attach/detach lifecycle, mirroring the React hook
54530
+ * `packages/react/src/viewer/hooks/useTouchGestures.ts`:
54531
+ * - `touchstart` / `touchmove` are registered with `{ passive: false }` so the
54532
+ * recogniser can call `preventDefault()` to suppress the browser's native
54533
+ * pinch-zoom on the canvas;
54534
+ * - `touchend` / `touchcancel` are passive (they never call preventDefault).
54535
+ *
54536
+ * `attachTouchGestures(element, config)` returns a teardown function that
54537
+ * removes every listener and cancels any pending long-press timer. Components
54538
+ * call it from `afterNextRender` (when the target node is live) and run the
54539
+ * teardown via `DestroyRef.onDestroy`.
54540
+ *
54541
+ * @module touch-gestures (angular)
54542
+ */
54543
+ /**
54544
+ * Viewer zoom bounds. The Angular viewer clamps zoom to [0.2, 3] (see
54545
+ * `ZOOM_MIN` / `ZOOM_MAX` in `power-point-viewer.component.ts`); the pinch path
54546
+ * must clamp to the same range so a pinch can never exceed the buttons.
54547
+ */
54548
+ const MIN_ZOOM_SCALE = 0.2;
54549
+ const MAX_ZOOM_SCALE = 3;
54550
+ /** Clamp a scale value to the Angular viewer's allowed zoom range. */
54551
+ function clampScale(value) {
54552
+ return clampScale$1(value, MIN_ZOOM_SCALE, MAX_ZOOM_SCALE);
54553
+ }
54554
+ /**
54555
+ * Attach the shared touch-gesture recogniser to `element` and return a teardown
54556
+ * function. Mirrors the React hook's listener lifecycle exactly:
54557
+ * `touchstart`/`touchmove` are non-passive (so pinch can `preventDefault`),
54558
+ * `touchend`/`touchcancel` are passive.
54559
+ */
54560
+ function attachTouchGestures(element, config) {
54561
+ const recognizer = createTouchGestureRecognizer({
54562
+ getScale: config.getScale,
54563
+ minScale: MIN_ZOOM_SCALE,
54564
+ maxScale: MAX_ZOOM_SCALE,
54565
+ callbacks: config.callbacks,
54566
+ });
54567
+ const onStart = (e) => recognizer.onTouchStart(e);
54568
+ const onMove = (e) => recognizer.onTouchMove(e);
54569
+ const onEnd = (e) => recognizer.onTouchEnd(e);
54570
+ const onCancel = () => recognizer.onTouchCancel();
54571
+ element.addEventListener('touchstart', onStart, { passive: false });
54572
+ element.addEventListener('touchmove', onMove, { passive: false });
54573
+ element.addEventListener('touchend', onEnd, { passive: true });
54574
+ element.addEventListener('touchcancel', onCancel, { passive: true });
54575
+ return () => {
54576
+ element.removeEventListener('touchstart', onStart);
54577
+ element.removeEventListener('touchmove', onMove);
54578
+ element.removeEventListener('touchend', onEnd);
54579
+ element.removeEventListener('touchcancel', onCancel);
54580
+ recognizer.cancel();
54581
+ };
54582
+ }
54583
+
54380
54584
  const ZOOM_STEP = 0.1;
54381
54585
  const ZOOM_MIN = 0.2;
54382
54586
  const ZOOM_MAX = 3;
@@ -54810,6 +55014,9 @@ class PowerPointViewerComponent {
54810
55014
  this.activeSession.set(null);
54811
55015
  }
54812
55016
  });
55017
+ // Attach multi-touch gestures (pinch-zoom / swipe-nav / long-press menu)
55018
+ // to the canvas host once it is rendered. See setupTouchGestures().
55019
+ this.setupTouchGestures();
54813
55020
  }
54814
55021
  /**
54815
55022
  * Serialise the current presentation to `.pptx` bytes (imperative handle).
@@ -54946,54 +55153,60 @@ class PowerPointViewerComponent {
54946
55153
  this.collab.disconnect();
54947
55154
  this.activeSession.set(null);
54948
55155
  }
54949
- /** Horizontal-swipe tracking start coordinates (touch begins on the canvas). */
54950
- swipeStartX = null;
54951
- swipeStartY = null;
54952
55156
  /**
54953
- * Begin tracking a horizontal swipe for slide navigation.
55157
+ * Wire the framework-agnostic touch-gesture recogniser to the `<main>` canvas
55158
+ * host once it is in the DOM. Mirrors React's `useTouchGestures` wiring:
55159
+ * - pinch-to-zoom always updates the zoom signal (clamped to the viewer
55160
+ * range), with `preventDefault()` on the pinch path to suppress the
55161
+ * browser's native pinch-zoom;
55162
+ * - horizontal swipe navigates slides, but only when editing is off
55163
+ * (`!canEdit()`): in edit mode single-finger gestures belong to element
55164
+ * manipulation (move/resize/rotate), so we never hijack them. The large
55165
+ * ‹ › buttons remain available for explicit navigation in all modes;
55166
+ * - long-press in edit mode opens the editor context menu at the press
55167
+ * point for the current selection (mirrors React's onLongPress path).
54954
55168
  *
54955
- * To disambiguate a navigation swipe from an element drag, swipe-nav is only
54956
- * armed when editing is off (`!canEdit()`). When `canEdit()` is true,
54957
- * pointer/touch gestures belong to element manipulation (move/resize/rotate),
54958
- * so we never hijack them. The large ‹ › buttons remain available in all
54959
- * modes for explicit navigation.
55169
+ * The recogniser's swipe/long-press callbacks check the live `canEdit()` /
55170
+ * selection state, so a single attach handles every mode without re-binding.
54960
55171
  */
54961
- onMainTouchStart(event) {
54962
- if (this.canEdit() || event.changedTouches.length !== 1) {
54963
- this.swipeStartX = null;
54964
- this.swipeStartY = null;
54965
- return;
54966
- }
54967
- const touch = event.changedTouches[0];
54968
- this.swipeStartX = touch.clientX;
54969
- this.swipeStartY = touch.clientY;
54970
- }
54971
- /**
54972
- * Complete a swipe: a predominantly horizontal drag of at least the threshold
54973
- * navigates to the previous (swipe right) or next (swipe left) slide.
54974
- */
54975
- onMainTouchEnd(event) {
54976
- const startX = this.swipeStartX;
54977
- const startY = this.swipeStartY;
54978
- this.swipeStartX = null;
54979
- this.swipeStartY = null;
54980
- if (startX === null || startY === null || event.changedTouches.length !== 1) {
54981
- return;
54982
- }
54983
- const touch = event.changedTouches[0];
54984
- const dx = touch.clientX - startX;
54985
- const dy = touch.clientY - startY;
54986
- const SWIPE_THRESHOLD = 50;
54987
- // Ignore vertical-dominant gestures (scrolling) and short drags.
54988
- if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dx) <= Math.abs(dy)) {
54989
- return;
54990
- }
54991
- if (dx < 0) {
54992
- this.goNext();
54993
- }
54994
- else {
54995
- this.goPrev();
54996
- }
55172
+ setupTouchGestures() {
55173
+ const destroyRef = inject(DestroyRef);
55174
+ afterNextRender(() => {
55175
+ const el = this.mainEl()?.nativeElement;
55176
+ if (!el) {
55177
+ return;
55178
+ }
55179
+ const teardown = attachTouchGestures(el, {
55180
+ getScale: () => this.zoom(),
55181
+ callbacks: {
55182
+ onPinchZoom: (newScale) => this.zoom.set(newScale),
55183
+ onSwipe: (direction) => {
55184
+ // Edit mode: leave single-finger gestures to element manipulation.
55185
+ if (this.canEdit()) {
55186
+ return;
55187
+ }
55188
+ // direction 1 = swipe right (previous), -1 = swipe left (next).
55189
+ if (direction === 1) {
55190
+ this.goPrev();
55191
+ }
55192
+ else {
55193
+ this.goNext();
55194
+ }
55195
+ },
55196
+ onLongPress: (x, y) => {
55197
+ if (!this.canEdit() || this.presenting()) {
55198
+ return;
55199
+ }
55200
+ const selected = this.selectedElement();
55201
+ if (!selected) {
55202
+ return;
55203
+ }
55204
+ this.contextMenuPos.set({ x, y });
55205
+ },
55206
+ },
55207
+ });
55208
+ destroyRef.onDestroy(teardown);
55209
+ });
54997
55210
  }
54998
55211
  zoomIn() {
54999
55212
  this.zoom.set(Math.min(ZOOM_MAX, Number((this.zoom() + ZOOM_STEP).toFixed(2))));
@@ -55791,12 +56004,7 @@ class PowerPointViewerComponent {
55791
56004
  </nav>
55792
56005
  }
55793
56006
 
55794
- <main
55795
- class="pptx-ng-main"
55796
- #mainEl
55797
- (touchstart)="onMainTouchStart($event)"
55798
- (touchend)="onMainTouchEnd($event)"
55799
- >
56007
+ <main class="pptx-ng-main" #mainEl>
55800
56008
  <pptx-slide-canvas
55801
56009
  [slide]="activeSlide()"
55802
56010
  [canvasSize]="loader.canvasSize()"
@@ -56361,12 +56569,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
56361
56569
  </nav>
56362
56570
  }
56363
56571
 
56364
- <main
56365
- class="pptx-ng-main"
56366
- #mainEl
56367
- (touchstart)="onMainTouchStart($event)"
56368
- (touchend)="onMainTouchEnd($event)"
56369
- >
56572
+ <main class="pptx-ng-main" #mainEl>
56370
56573
  <pptx-slide-canvas
56371
56574
  [slide]="activeSlide()"
56372
56575
  [canvasSize]="loader.canvasSize()"