pptx-angular-viewer 1.1.49 → 1.1.51

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.
@@ -29879,6 +29879,163 @@ function createDefaultChartElement(chartType = DEFAULT_INSERT_CHART_TYPE, positi
29879
29879
  });
29880
29880
  }
29881
29881
 
29882
+ /**
29883
+ * SmartArt reflow: convert algorithmic layout results back to
29884
+ * PptxSmartArtDrawingShape[] so the drawing-shape renderer handles post-edit
29885
+ * display and shapes round-trip correctly through save.
29886
+ *
29887
+ * When structural edits (add/remove/reorder/text/style) clear `drawingShapes`,
29888
+ * `rebuildDrawingShapesIfCleared` calls the layout engine and converts the
29889
+ * geometry back to drawing shapes so the richer DrawingShapeRenderer path
29890
+ * stays active rather than falling back to the plain SVG family renderer.
29891
+ *
29892
+ * @module smartart-reflow-to-shapes
29893
+ */
29894
+ // ── Polygon bounding-box helper ───────────────────────────────────────────────
29895
+ /**
29896
+ * Compute the axis-aligned bounding box of an SVG polygon points string.
29897
+ * Returns a zero-size box when the string is empty or unparseable.
29898
+ */
29899
+ function polygonBounds(points) {
29900
+ const pairs = points.trim().split(/\s+/u);
29901
+ let minX = Infinity;
29902
+ let minY = Infinity;
29903
+ let maxX = -Infinity;
29904
+ let maxY = -Infinity;
29905
+ for (const pair of pairs) {
29906
+ const comma = pair.indexOf(',');
29907
+ if (comma < 0) {
29908
+ continue;
29909
+ }
29910
+ const x = parseFloat(pair.slice(0, comma));
29911
+ const y = parseFloat(pair.slice(comma + 1));
29912
+ if (!isFinite(x) || !isFinite(y)) {
29913
+ continue;
29914
+ }
29915
+ if (x < minX) {
29916
+ minX = x;
29917
+ }
29918
+ if (x > maxX) {
29919
+ maxX = x;
29920
+ }
29921
+ if (y < minY) {
29922
+ minY = y;
29923
+ }
29924
+ if (y > maxY) {
29925
+ maxY = y;
29926
+ }
29927
+ }
29928
+ if (!isFinite(minX)) {
29929
+ return { x: 0, y: 0, width: 0, height: 0 };
29930
+ }
29931
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
29932
+ }
29933
+ // ── Single-node mapping ────────────────────────────────────────────────────────
29934
+ /**
29935
+ * Map one RenderedNode to a PptxSmartArtDrawingShape.
29936
+ *
29937
+ * The shape ID encodes the layout family and the source node id as a suffix so
29938
+ * `resolveDrawingShapeNodeId` can match it back via the `reflow-` prefix rule.
29939
+ */
29940
+ function renderedNodeToShape(rn, nodeId, family) {
29941
+ const id = `reflow-${family}-${nodeId}`;
29942
+ const text = rn.text || undefined;
29943
+ if (rn.kind === 'rect') {
29944
+ return {
29945
+ id,
29946
+ shapeType: rn.rx > 0 ? 'roundRect' : 'rect',
29947
+ x: rn.x,
29948
+ y: rn.y,
29949
+ width: rn.width,
29950
+ height: rn.height,
29951
+ rotation: 0,
29952
+ fillColor: rn.fill,
29953
+ strokeColor: rn.stroke,
29954
+ strokeWidth: rn.strokeWidth,
29955
+ text,
29956
+ fontSize: rn.fontSize,
29957
+ };
29958
+ }
29959
+ if (rn.kind === 'circle') {
29960
+ return {
29961
+ id,
29962
+ shapeType: 'ellipse',
29963
+ x: rn.cx - rn.r,
29964
+ y: rn.cy - rn.r,
29965
+ width: rn.r * 2,
29966
+ height: rn.r * 2,
29967
+ rotation: 0,
29968
+ fillColor: rn.fill,
29969
+ strokeColor: rn.stroke,
29970
+ strokeWidth: rn.strokeWidth,
29971
+ text,
29972
+ fontSize: rn.fontSize,
29973
+ };
29974
+ }
29975
+ // polygon -- extract a bounding box from the SVG points string
29976
+ const bounds = polygonBounds(rn.points);
29977
+ return {
29978
+ id,
29979
+ shapeType: 'chevron',
29980
+ x: bounds.x,
29981
+ y: bounds.y,
29982
+ width: bounds.width,
29983
+ height: bounds.height,
29984
+ rotation: 0,
29985
+ fillColor: rn.fill,
29986
+ strokeColor: rn.stroke,
29987
+ strokeWidth: rn.strokeWidth,
29988
+ text,
29989
+ fontSize: rn.fontSize,
29990
+ };
29991
+ }
29992
+ // ── Public API ─────────────────────────────────────────────────────────────────
29993
+ /**
29994
+ * Convert a SmartArt layout result to an array of PptxSmartArtDrawingShape.
29995
+ *
29996
+ * Rendered nodes are matched to source nodes by flat index when the counts
29997
+ * align; otherwise the rendered node's key is used as the node-id suffix.
29998
+ * Connector paths are omitted: PptxSmartArtDrawingShape has no SVG path field,
29999
+ * and connectors carry no editable content anyway.
30000
+ */
30001
+ function reflowToDrawingShapes(layoutResult, nodes) {
30002
+ const flat = flattenNodes([...nodes]);
30003
+ const family = layoutResult.family;
30004
+ const shapes = [];
30005
+ for (let i = 0; i < layoutResult.nodes.length; i++) {
30006
+ const rn = layoutResult.nodes[i];
30007
+ const sourceNode = flat[i];
30008
+ const nodeId = sourceNode?.id ?? rn.key;
30009
+ shapes.push(renderedNodeToShape(rn, nodeId, family));
30010
+ }
30011
+ return shapes;
30012
+ }
30013
+ /**
30014
+ * Rebuild drawing shapes from the algorithmic layout engine when they have been
30015
+ * cleared by a structural edit (add/remove/reorder/text/style change on a node).
30016
+ *
30017
+ * Returns the original data unchanged when drawing shapes are already populated
30018
+ * or when there are no nodes to lay out.
30019
+ *
30020
+ * @param smartArtData - The updated SmartArt data (with cleared drawingShapes).
30021
+ * @param layout - Named layout preset from the element (may be undefined).
30022
+ * @param palette - Resolved colour palette (hex strings).
30023
+ * @param style - Resolved SmartArt style intensity.
30024
+ * @param elementId - Element ID used for stable SVG key generation.
30025
+ * @param box - Pixel bounding box of the element.
30026
+ */
30027
+ function rebuildDrawingShapesIfCleared(smartArtData, layout, palette, style, elementId, box) {
30028
+ const shapes = smartArtData.drawingShapes;
30029
+ if ((shapes !== undefined && shapes.length > 0) || smartArtData.nodes.length === 0) {
30030
+ return smartArtData;
30031
+ }
30032
+ const layoutResult = computeSmartArtLayout(smartArtData.nodes, box, palette, style, elementId, smartArtData.resolvedLayoutType, layout);
30033
+ return {
30034
+ ...smartArtData,
30035
+ drawingShapes: reflowToDrawingShapes(layoutResult, smartArtData.nodes),
30036
+ };
30037
+ }
30038
+
29882
30039
  /**
29883
30040
  * Framework-agnostic rendering & editing helpers shared by the React, Vue, and
29884
30041
  * Angular `pptx-viewer` bindings. Pure TypeScript (no framework imports) — each
@@ -35644,7 +35801,7 @@ class EditorToolbarComponent {
35644
35801
  </button>
35645
35802
  </div>
35646
35803
  </div>
35647
- `, isInline: true, styles: [".pptx-ng-toolbar{display:flex;flex-direction:row;align-items:center;gap:0;padding:4px 8px;background:var(--pptx-toolbar-bg, #1e1e1e);color:var(--pptx-toolbar-fg, #e0e0e0);border-bottom:1px solid var(--pptx-toolbar-border, #333);min-height:36px;-webkit-user-select:none;user-select:none}.pptx-ng-toolbar__group{display:flex;flex-direction:row;align-items:center;gap:2px}.pptx-ng-toolbar__group-label{font-size:10px;color:var(--pptx-toolbar-muted, #888);text-transform:uppercase;letter-spacing:.05em;padding:0 6px 0 4px;flex-shrink:0}.pptx-ng-toolbar__divider{width:1px;height:20px;background:var(--pptx-toolbar-border, #444);margin:0 6px;flex-shrink:0}.pptx-ng-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:26px;padding:0;background:transparent;border:1px solid transparent;border-radius:3px;color:inherit;font-size:14px;cursor:pointer;transition:background .1s,border-color .1s;flex-shrink:0}.pptx-ng-toolbar__btn:hover:not(:disabled){background:var(--pptx-toolbar-hover, #3a3a3a);border-color:var(--pptx-toolbar-border, #555)}.pptx-ng-toolbar__btn:active:not(:disabled){background:var(--pptx-toolbar-active-bg, #2a2a2a)}.pptx-ng-toolbar__btn:disabled{opacity:.35;cursor:default}.pptx-ng-toolbar__btn--danger:not(:disabled){color:var(--pptx-toolbar-danger, #f47c7c)}.pptx-ng-toolbar__btn--danger:hover:not(:disabled){background:var(--pptx-toolbar-danger-hover, #4a1a1a);border-color:var(--pptx-toolbar-danger-border, #6b2a2a)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35804
+ `, isInline: true, styles: [".pptx-ng-toolbar{display:flex;flex-direction:row;align-items:center;gap:0;padding:4px 8px;background:var(--pptx-toolbar-bg, #1e1e1e);color:var(--pptx-toolbar-fg, #e0e0e0);border-bottom:1px solid var(--pptx-toolbar-border, #333);min-height:36px;-webkit-user-select:none;user-select:none}.pptx-ng-toolbar__group{display:flex;flex-direction:row;align-items:center;gap:4px}.pptx-ng-toolbar__group-label{font-size:10px;color:var(--pptx-toolbar-muted, #888);text-transform:uppercase;letter-spacing:.05em;padding:0 6px 0 4px;flex-shrink:0}.pptx-ng-toolbar__divider{width:1px;height:20px;background:var(--pptx-toolbar-border, #444);margin:0 4px;flex-shrink:0}.pptx-ng-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:2px 8px;background:transparent;border:1px solid transparent;border-radius:4px;color:inherit;font-size:14px;cursor:pointer;transition:background .1s;flex-shrink:0}.pptx-ng-toolbar__btn:hover:not(:disabled){background:var(--pptx-toolbar-hover, #3a3a3a)}.pptx-ng-toolbar__btn:active:not(:disabled){background:var(--pptx-toolbar-active-bg, #2a2a2a);transform:scale(.95);opacity:.8}.pptx-ng-toolbar__btn:disabled{opacity:.4;cursor:not-allowed}.pptx-ng-toolbar__btn--danger:not(:disabled){color:var(--pptx-toolbar-danger, #f47c7c)}.pptx-ng-toolbar__btn--danger:hover:not(:disabled){background:var(--pptx-toolbar-danger-hover, #4a1a1a)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35648
35805
  }
35649
35806
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EditorToolbarComponent, decorators: [{
35650
35807
  type: Component,
@@ -35855,7 +36012,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
35855
36012
  </button>
35856
36013
  </div>
35857
36014
  </div>
35858
- `, styles: [".pptx-ng-toolbar{display:flex;flex-direction:row;align-items:center;gap:0;padding:4px 8px;background:var(--pptx-toolbar-bg, #1e1e1e);color:var(--pptx-toolbar-fg, #e0e0e0);border-bottom:1px solid var(--pptx-toolbar-border, #333);min-height:36px;-webkit-user-select:none;user-select:none}.pptx-ng-toolbar__group{display:flex;flex-direction:row;align-items:center;gap:2px}.pptx-ng-toolbar__group-label{font-size:10px;color:var(--pptx-toolbar-muted, #888);text-transform:uppercase;letter-spacing:.05em;padding:0 6px 0 4px;flex-shrink:0}.pptx-ng-toolbar__divider{width:1px;height:20px;background:var(--pptx-toolbar-border, #444);margin:0 6px;flex-shrink:0}.pptx-ng-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:26px;padding:0;background:transparent;border:1px solid transparent;border-radius:3px;color:inherit;font-size:14px;cursor:pointer;transition:background .1s,border-color .1s;flex-shrink:0}.pptx-ng-toolbar__btn:hover:not(:disabled){background:var(--pptx-toolbar-hover, #3a3a3a);border-color:var(--pptx-toolbar-border, #555)}.pptx-ng-toolbar__btn:active:not(:disabled){background:var(--pptx-toolbar-active-bg, #2a2a2a)}.pptx-ng-toolbar__btn:disabled{opacity:.35;cursor:default}.pptx-ng-toolbar__btn--danger:not(:disabled){color:var(--pptx-toolbar-danger, #f47c7c)}.pptx-ng-toolbar__btn--danger:hover:not(:disabled){background:var(--pptx-toolbar-danger-hover, #4a1a1a);border-color:var(--pptx-toolbar-danger-border, #6b2a2a)}\n"] }]
36015
+ `, styles: [".pptx-ng-toolbar{display:flex;flex-direction:row;align-items:center;gap:0;padding:4px 8px;background:var(--pptx-toolbar-bg, #1e1e1e);color:var(--pptx-toolbar-fg, #e0e0e0);border-bottom:1px solid var(--pptx-toolbar-border, #333);min-height:36px;-webkit-user-select:none;user-select:none}.pptx-ng-toolbar__group{display:flex;flex-direction:row;align-items:center;gap:4px}.pptx-ng-toolbar__group-label{font-size:10px;color:var(--pptx-toolbar-muted, #888);text-transform:uppercase;letter-spacing:.05em;padding:0 6px 0 4px;flex-shrink:0}.pptx-ng-toolbar__divider{width:1px;height:20px;background:var(--pptx-toolbar-border, #444);margin:0 4px;flex-shrink:0}.pptx-ng-toolbar__btn{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:2px 8px;background:transparent;border:1px solid transparent;border-radius:4px;color:inherit;font-size:14px;cursor:pointer;transition:background .1s;flex-shrink:0}.pptx-ng-toolbar__btn:hover:not(:disabled){background:var(--pptx-toolbar-hover, #3a3a3a)}.pptx-ng-toolbar__btn:active:not(:disabled){background:var(--pptx-toolbar-active-bg, #2a2a2a);transform:scale(.95);opacity:.8}.pptx-ng-toolbar__btn:disabled{opacity:.4;cursor:not-allowed}.pptx-ng-toolbar__btn--danger:not(:disabled){color:var(--pptx-toolbar-danger, #f47c7c)}.pptx-ng-toolbar__btn--danger:hover:not(:disabled){background:var(--pptx-toolbar-danger-hover, #4a1a1a)}\n"] }]
35859
36016
  }], propDecorators: { slideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "slideIndex", required: true }] }] } });
35860
36017
 
35861
36018
  /**
@@ -36410,10 +36567,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
36410
36567
  }] });
36411
36568
 
36412
36569
  /**
36413
- * Native EyeDropper API wrapper for the Angular viewer.
36570
+ * EyeDropper colour sampler for the Angular viewer.
36414
36571
  *
36415
- * Feature-detects the browser EyeDropper API (Chrome 95+ / Edge 95+).
36416
- * When unavailable, all functions no-op gracefully.
36572
+ * Prefers the native browser EyeDropper API (Chrome 95+ / Edge 95+). For
36573
+ * browsers without it (Firefox, Safari) a DOM-sampling fallback reads the
36574
+ * colour under the pointer via `elementFromPoint` + `getComputedStyle`, so the
36575
+ * eyedropper still works everywhere. Mirrors the React viewer's
36576
+ * `viewer/utils/eyedropper.ts`.
36417
36577
  */
36418
36578
  /**
36419
36579
  * Returns true when the native EyeDropper API is available in this browser.
@@ -36441,6 +36601,103 @@ async function openNativeEyeDropper() {
36441
36601
  return null;
36442
36602
  }
36443
36603
  }
36604
+ // ---------------------------------------------------------------------------
36605
+ // Fallback sampling (Firefox / Safari)
36606
+ // ---------------------------------------------------------------------------
36607
+ function toHex(r, g, b) {
36608
+ const h = (n) => n.toString(16).padStart(2, '0');
36609
+ return `#${h(r)}${h(g)}${h(b)}`;
36610
+ }
36611
+ function parseRgbaString(str) {
36612
+ const match = str.match(/rgba?\(\s*(?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)/u);
36613
+ if (!match?.groups) {
36614
+ return null;
36615
+ }
36616
+ const r = parseInt(match.groups.r, 10);
36617
+ const g = parseInt(match.groups.g, 10);
36618
+ const b = parseInt(match.groups.b, 10);
36619
+ return { r, g, b, hex: toHex(r, g, b) };
36620
+ }
36621
+ /**
36622
+ * Sample the colour at a client-space point by inspecting the topmost element's
36623
+ * computed paint. Tries, in order: a real `<canvas>` pixel read (sharp for
36624
+ * rasterised content), then the element's `background-color`, SVG `fill`, and
36625
+ * finally text `color`. Returns `null` when nothing paintable is found.
36626
+ *
36627
+ * This is the DOM fallback used when the native EyeDropper API is unavailable;
36628
+ * it needs no canvas plumbing, so it works against the live slide DOM.
36629
+ */
36630
+ function sampleColorFromSlide(clientX, clientY) {
36631
+ const target = typeof document === 'undefined' ? null : document.elementFromPoint(clientX, clientY);
36632
+ if (!(target instanceof Element)) {
36633
+ return null;
36634
+ }
36635
+ // Direct pixel read when the pointer is over a <canvas> (untainted only).
36636
+ const canvas = target instanceof HTMLCanvasElement ? target : target.closest('canvas');
36637
+ if (canvas instanceof HTMLCanvasElement) {
36638
+ try {
36639
+ const ctx = canvas.getContext('2d');
36640
+ if (ctx) {
36641
+ const rect = canvas.getBoundingClientRect();
36642
+ const sx = Math.round((clientX - rect.left) * (canvas.width / canvas.clientWidth));
36643
+ const sy = Math.round((clientY - rect.top) * (canvas.height / canvas.clientHeight));
36644
+ const pixel = ctx.getImageData(sx, sy, 1, 1).data;
36645
+ return { r: pixel[0], g: pixel[1], b: pixel[2], hex: toHex(pixel[0], pixel[1], pixel[2]) };
36646
+ }
36647
+ }
36648
+ catch {
36649
+ // Cross-origin / tainted canvas: fall through to computed-style sampling.
36650
+ }
36651
+ }
36652
+ const computed = getComputedStyle(target);
36653
+ const bg = computed.backgroundColor;
36654
+ if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') {
36655
+ const parsed = parseRgbaString(bg);
36656
+ if (parsed) {
36657
+ return parsed;
36658
+ }
36659
+ }
36660
+ const fill = computed.fill;
36661
+ if (fill && fill !== 'none' && fill !== 'transparent') {
36662
+ const parsed = parseRgbaString(fill);
36663
+ if (parsed) {
36664
+ return parsed;
36665
+ }
36666
+ }
36667
+ return computed.color ? parseRgbaString(computed.color) : null;
36668
+ }
36669
+ /**
36670
+ * Run the eyedropper fallback: arm a one-shot pointer listener and resolve with
36671
+ * the hex colour of the next click (or `null` if the user presses Escape). Used
36672
+ * only when {@link eyedropperAvailable} is false. The caller is responsible for
36673
+ * any "armed" UI affordance; this just manages the listeners.
36674
+ */
36675
+ function pickColorByClickFallback() {
36676
+ if (typeof document === 'undefined') {
36677
+ return Promise.resolve(null);
36678
+ }
36679
+ return new Promise((resolve) => {
36680
+ const cleanup = () => {
36681
+ document.removeEventListener('pointerdown', onPointerDown, true);
36682
+ document.removeEventListener('keydown', onKeyDown, true);
36683
+ };
36684
+ const onPointerDown = (event) => {
36685
+ event.preventDefault();
36686
+ event.stopPropagation();
36687
+ cleanup();
36688
+ const sample = sampleColorFromSlide(event.clientX, event.clientY);
36689
+ resolve(sample ? sample.hex : null);
36690
+ };
36691
+ const onKeyDown = (event) => {
36692
+ if (event.key === 'Escape') {
36693
+ cleanup();
36694
+ resolve(null);
36695
+ }
36696
+ };
36697
+ document.addEventListener('pointerdown', onPointerDown, true);
36698
+ document.addEventListener('keydown', onKeyDown, true);
36699
+ });
36700
+ }
36444
36701
 
36445
36702
  /**
36446
36703
  * `LoadContentService`: Angular port of the React `useLoadContent` hook and
@@ -45069,6 +45326,9 @@ class InspectorPanelComponent {
45069
45326
  };
45070
45327
  }, /* @ts-ignore */
45071
45328
  ...(ngDevMode ? [{ debugName: "seed" }] : /* istanbul ignore next */ []));
45329
+ /** Whether the element has lock flags preventing move/select. */
45330
+ isLocked = computed(() => Boolean(this.el().locks?.noMove) || Boolean(this.el().locks?.noSelect), /* @ts-ignore */
45331
+ ...(ngDevMode ? [{ debugName: "isLocked" }] : /* istanbul ignore next */ []));
45072
45332
  /** Whether the element supports shape-style (fill/stroke) editing. */
45073
45333
  hasShape = computed(() => hasShapeProperties(this.el()), /* @ts-ignore */
45074
45334
  ...(ngDevMode ? [{ debugName: "hasShape" }] : /* istanbul ignore next */ []));
@@ -45105,6 +45365,16 @@ class InspectorPanelComponent {
45105
45365
  smartArtData,
45106
45366
  });
45107
45367
  }
45368
+ /** Toggle element lock (noMove + noResize + noSelect). */
45369
+ onLockToggle() {
45370
+ if (this.isLocked()) {
45371
+ this.onPatch({ locks: undefined });
45372
+ }
45373
+ else {
45374
+ const locks = { noMove: true, noResize: true, noSelect: true };
45375
+ this.onPatch({ locks });
45376
+ }
45377
+ }
45108
45378
  /** Commit a partial-element patch from an advanced sub-panel as one history entry. */
45109
45379
  onPatch(patch) {
45110
45380
  this.editor.updateElement(this.slideIndex(), this.el().id, patch);
@@ -45384,7 +45654,22 @@ class InspectorPanelComponent {
45384
45654
 
45385
45655
  <!-- ── Arrange ────────────────────────────────────────────────────── -->
45386
45656
  <section class="pptx-ng-inspector__section">
45387
- <h3 class="pptx-ng-inspector__heading">Arrange</h3>
45657
+ <div
45658
+ class="pptx-ng-inspector__row"
45659
+ style="justify-content:space-between;margin-bottom:0.35rem"
45660
+ >
45661
+ <h3 class="pptx-ng-inspector__heading" style="margin:0">Arrange</h3>
45662
+ <button
45663
+ type="button"
45664
+ class="pptx-ng-inspector__btn"
45665
+ style="flex:0 0 auto;padding:2px 6px"
45666
+ [attr.aria-pressed]="isLocked()"
45667
+ [title]="isLocked() ? 'Unlock element' : 'Lock element'"
45668
+ (click)="onLockToggle()"
45669
+ >
45670
+ {{ isLocked() ? 'Locked' : 'Lock' }}
45671
+ </button>
45672
+ </div>
45388
45673
 
45389
45674
  <div class="pptx-ng-inspector__row">
45390
45675
  <button
@@ -45689,7 +45974,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
45689
45974
 
45690
45975
  <!-- ── Arrange ────────────────────────────────────────────────────── -->
45691
45976
  <section class="pptx-ng-inspector__section">
45692
- <h3 class="pptx-ng-inspector__heading">Arrange</h3>
45977
+ <div
45978
+ class="pptx-ng-inspector__row"
45979
+ style="justify-content:space-between;margin-bottom:0.35rem"
45980
+ >
45981
+ <h3 class="pptx-ng-inspector__heading" style="margin:0">Arrange</h3>
45982
+ <button
45983
+ type="button"
45984
+ class="pptx-ng-inspector__btn"
45985
+ style="flex:0 0 auto;padding:2px 6px"
45986
+ [attr.aria-pressed]="isLocked()"
45987
+ [title]="isLocked() ? 'Unlock element' : 'Lock element'"
45988
+ (click)="onLockToggle()"
45989
+ >
45990
+ {{ isLocked() ? 'Locked' : 'Lock' }}
45991
+ </button>
45992
+ </div>
45693
45993
 
45694
45994
  <div class="pptx-ng-inspector__row">
45695
45995
  <button
@@ -49507,20 +49807,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
49507
49807
  `, styles: [".pptx-ng-ole-preview{position:relative;width:100%;height:100%}.pptx-ng-ole-img{width:100%;height:100%;object-fit:contain;pointer-events:none;-webkit-user-select:none;user-select:none;display:block}.pptx-ng-ole-badge{position:absolute;bottom:4px;right:4px;z-index:10}.pptx-ng-ole-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box}.pptx-ng-ole-name{margin-top:8px;font-size:12px;font-weight:500;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-sublabel{margin-top:2px;font-size:10px;color:#00000073;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-actions{position:absolute;bottom:4px;left:4px;display:flex;gap:4px;z-index:11;opacity:0;transition:opacity .12s ease-in-out}.pptx-ng-ole:hover .pptx-ng-ole-actions,.pptx-ng-ole-actions:focus-within{opacity:1}.pptx-ng-ole-action{font-size:11px;line-height:1;padding:4px 8px;border-radius:4px;background-color:#000000b8;color:#fff;text-decoration:none;cursor:pointer;white-space:nowrap;pointer-events:auto}.pptx-ng-ole-action:hover{background-color:#000000d9}.pptx-ng-ole-action:focus-visible{outline:2px solid #fff;outline-offset:1px}\n"] }]
49508
49808
  }], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }] } });
49509
49809
 
49510
- /**
49511
- * Thin re-export shim → vendored `pptx-viewer-shared` (`render/smartart-drawing`).
49512
- *
49513
- * The drawing-shape view-model helpers (palette resolution, chrome style,
49514
- * viewBox fitting, `RenderedShape` projection) were extracted to shared and are
49515
- * consumed by every binding. This shim preserves the historical Angular import
49516
- * surface.
49517
- *
49518
- * Shared exports the SmartArt default palette as `SMARTART_DEFAULT_PALETTE`
49519
- * (the bare `DEFAULT_PALETTE` is taken by the chart palette in shared); it is
49520
- * re-aliased back to `DEFAULT_PALETTE` here so this binding's import sites and
49521
- * the colocated tests are unchanged.
49522
- */
49523
-
49524
49810
  /**
49525
49811
  * smart-art-inline-edit.ts: pure logic for on-canvas SmartArt node text editing.
49526
49812
  *
@@ -49691,20 +49977,65 @@ function containsElementId(elements, id) {
49691
49977
  return false;
49692
49978
  }
49693
49979
 
49980
+ /**
49981
+ * Thin re-export shim → vendored `pptx-viewer-shared` (`render/smartart-drawing`).
49982
+ *
49983
+ * The drawing-shape view-model helpers (palette resolution, chrome style,
49984
+ * viewBox fitting, `RenderedShape` projection) were extracted to shared and are
49985
+ * consumed by every binding. This shim preserves the historical Angular import
49986
+ * surface.
49987
+ *
49988
+ * Shared exports the SmartArt default palette as `SMARTART_DEFAULT_PALETTE`
49989
+ * (the bare `DEFAULT_PALETTE` is taken by the chart palette in shared); it is
49990
+ * re-aliased back to `DEFAULT_PALETTE` here so this binding's import sites and
49991
+ * the colocated tests are unchanged.
49992
+ */
49993
+
49994
+ /** Narrow a RenderedNode to a circle, or undefined. */
49995
+ function narrowToCircle(node) {
49996
+ return node.kind === 'circle' ? node : undefined;
49997
+ }
49998
+ /** Narrow a RenderedNode to a polygon, or undefined. */
49999
+ function narrowToPolygon(node) {
50000
+ return node.kind === 'polygon' ? node : undefined;
50001
+ }
50002
+ /** Narrow a RenderedNode to a rect, or undefined. */
50003
+ function narrowToRect(node) {
50004
+ return node.kind === 'rect' ? node : undefined;
50005
+ }
50006
+ /**
50007
+ * Split node text on newlines and compute per-line y offsets (in SVG px)
50008
+ * that centre the block around the node centre y (offset 0). Single-line
50009
+ * text produces one entry with offsetY=0, preserving the existing
50010
+ * dominant-baseline="central" behaviour exactly.
50011
+ */
50012
+ function computeTextLines(text, fontSize) {
50013
+ const raw = (text ?? '').split('\n').filter((l) => l.length > 0);
50014
+ if (raw.length === 0) {
50015
+ return [{ text: '', offsetY: 0 }];
50016
+ }
50017
+ const lh = fontSize * 1.2;
50018
+ const totalH = raw.length * lh;
50019
+ return raw.map((line, i) => ({
50020
+ text: line,
50021
+ offsetY: -totalH / 2 + lh / 2 + i * lh,
50022
+ }));
50023
+ }
50024
+
49694
50025
  /**
49695
50026
  * SmartArtRendererComponent: Angular SmartArt renderer.
49696
50027
  *
49697
50028
  * Data path mirrors the Vue `SmartArtRenderer.vue` and the React renderer:
49698
- * 1. **Drawing shapes** (`smartArtData.drawingShapes`) the preferred path
50029
+ * 1. **Drawing shapes** (`smartArtData.drawingShapes`) -- the preferred path
49699
50030
  * when the core extracted per-shape geometry from `ppt/diagrams/drawing*.xml`.
49700
- * 2. **Shared SVG-fallback engine** (`computeSmartArtLayout`) when no drawing
50031
+ * 2. **Shared SVG-fallback engine** (`computeSmartArtLayout`) -- when no drawing
49701
50032
  * shapes exist, the framework-agnostic engine in `pptx-viewer-shared`
49702
50033
  * positions/styles the node tree across all 10 layout families (list /
49703
50034
  * process / cycle / hierarchy / matrix / radial / pyramid / venn / funnel /
49704
50035
  * target), returning `RenderedNode[]` (rect / circle / polygon) +
49705
50036
  * `RenderedConnector[]` view-models. Every binding renders the same
49706
50037
  * geometry; this maps those view-models to SVG exactly as Vue does.
49707
- * 3. **Placeholder** when there is neither data nor any nodes/shapes.
50038
+ * 3. **Placeholder** -- when there is neither data nor any nodes/shapes.
49708
50039
  */
49709
50040
  class SmartArtRendererComponent {
49710
50041
  /** The smartArt element to render. Must be `type === 'smartArt'`. */
@@ -49733,6 +50064,9 @@ class SmartArtRendererComponent {
49733
50064
  /** The mounted `<textarea>` for the active node edit, if any. */
49734
50065
  nodeEditor = viewChild('nodeEditor', /* @ts-ignore */
49735
50066
  ...(ngDevMode ? [{ debugName: "nodeEditor" }] : /* istanbul ignore next */ []));
50067
+ /** Container div ref used to project hover rects into local coordinates. */
50068
+ smartartContainer = viewChild('smartartContainer', /* @ts-ignore */
50069
+ ...(ngDevMode ? [{ debugName: "smartartContainer" }] : /* istanbul ignore next */ []));
49736
50070
  /**
49737
50071
  * Guards against a cancel-triggered DOM-removal blur committing the edit.
49738
50072
  * Set to true before programmatic cancellation; reset to false on each new edit.
@@ -49787,6 +50121,17 @@ class SmartArtRendererComponent {
49787
50121
  ...(ngDevMode ? [{ debugName: "svgViewBox" }] : /* istanbul ignore next */ []));
49788
50122
  renderedShapes = computed(() => projectDrawingShapes(this.element().id, this.rawDrawingShapes(), this.viewBox(), this.palette(), this.artStyle()), /* @ts-ignore */
49789
50123
  ...(ngDevMode ? [{ debugName: "renderedShapes" }] : /* istanbul ignore next */ []));
50124
+ /**
50125
+ * Node id for each drawing shape (index-aligned with `renderedShapes`).
50126
+ * Used to tag `<g>` elements with `data-smartart-node-id` so the 3D
50127
+ * renderer's hit-test overlay can resolve a click to a node.
50128
+ */
50129
+ drawingShapeNodeIds = computed(() => {
50130
+ const shapes = this.rawDrawingShapes();
50131
+ const nodes = this.nodes();
50132
+ return shapes.map((shape, i) => resolveDrawingShapeNodeId(shape, i, shapes, nodes));
50133
+ }, /* @ts-ignore */
50134
+ ...(ngDevMode ? [{ debugName: "drawingShapeNodeIds" }] : /* istanbul ignore next */ []));
49790
50135
  // ── Shared SVG-fallback engine (no drawing shapes) ──────────────────────
49791
50136
  layout = computed(() => {
49792
50137
  const el = this.element();
@@ -49816,9 +50161,17 @@ class SmartArtRendererComponent {
49816
50161
  return map;
49817
50162
  }, /* @ts-ignore */
49818
50163
  ...(ngDevMode ? [{ debugName: "a11yLabelById" }] : /* istanbul ignore next */ []));
50164
+ /**
50165
+ * Parsed data-model node id for a rendered node (or `null` when the key does
50166
+ * not map to one). Exposed as a method so the template can use it: Angular
50167
+ * AOT templates can only call component members, not imported functions.
50168
+ */
50169
+ nodeKeyId(node) {
50170
+ return nodeIdFromKey(node.key, this.element().id);
50171
+ }
49819
50172
  /** Resolve the accessibility label for a rendered node (by parsed node id). */
49820
50173
  nodeAriaLabel(node) {
49821
- const nodeId = nodeIdFromKey(node.key, this.element().id);
50174
+ const nodeId = this.nodeKeyId(node);
49822
50175
  if (nodeId === null) {
49823
50176
  return null;
49824
50177
  }
@@ -49830,36 +50183,15 @@ class SmartArtRendererComponent {
49830
50183
  */
49831
50184
  liveMessage = signal('', /* @ts-ignore */
49832
50185
  ...(ngDevMode ? [{ debugName: "liveMessage" }] : /* istanbul ignore next */ []));
49833
- /** Narrow a `RenderedNode` to a circle, or `undefined`. */
49834
- asCircle(node) {
49835
- return node.kind === 'circle' ? node : undefined;
49836
- }
49837
- /** Narrow a `RenderedNode` to a polygon, or `undefined`. */
49838
- asPolygon(node) {
49839
- return node.kind === 'polygon' ? node : undefined;
49840
- }
49841
- /** Narrow a `RenderedNode` to a rect, or `undefined`. */
49842
- asRect(node) {
49843
- return node.kind === 'rect' ? node : undefined;
49844
- }
49845
- /**
49846
- * Split node text on `\n` and compute per-line y offsets (in SVG px) that
49847
- * centre the block around the node centre y (offset 0). Single-line text
49848
- * produces one entry with offsetY=0, preserving the existing
49849
- * `dominant-baseline="central"` behaviour exactly.
49850
- */
49851
- textLines(text, fontSize) {
49852
- const raw = (text ?? '').split('\n').filter((l) => l.length > 0);
49853
- if (raw.length === 0) {
49854
- return [{ text: '', offsetY: 0 }];
49855
- }
49856
- const lh = fontSize * 1.2;
49857
- const totalH = raw.length * lh;
49858
- return raw.map((line, i) => ({
49859
- text: line,
49860
- offsetY: -totalH / 2 + lh / 2 + i * lh,
49861
- }));
49862
- }
50186
+ hoveredNodeId = signal(null, /* @ts-ignore */
50187
+ ...(ngDevMode ? [{ debugName: "hoveredNodeId" }] : /* istanbul ignore next */ []));
50188
+ hoveredNodeRect = signal(null, /* @ts-ignore */
50189
+ ...(ngDevMode ? [{ debugName: "hoveredNodeRect" }] : /* istanbul ignore next */ []));
50190
+ /** Narrowing helpers bound as class properties for template type-checking. */
50191
+ asCircle = narrowToCircle;
50192
+ asPolygon = narrowToPolygon;
50193
+ asRect = narrowToRect;
50194
+ textLines = computeTextLines;
49863
50195
  // ── Inline node-text editing ───────────────────────────────────────────
49864
50196
  /** Double-click a node enters inline edit mode (when editable). */
49865
50197
  onNodeDblClick(event, node) {
@@ -49918,7 +50250,7 @@ class SmartArtRendererComponent {
49918
50250
  }
49919
50251
  /** The node's full (untruncated) data-model text, falling back to rendered text. */
49920
50252
  rawNodeText(node) {
49921
- const nodeId = nodeIdFromKey(node.key, this.element().id);
50253
+ const nodeId = this.nodeKeyId(node);
49922
50254
  if (nodeId === null) {
49923
50255
  return node.text;
49924
50256
  }
@@ -49946,21 +50278,81 @@ class SmartArtRendererComponent {
49946
50278
  // Announce the commit to assistive technology via the polite live region.
49947
50279
  this.liveMessage.set(text.trim().length > 0 ? `Node updated to ${text.trim()}` : 'Node cleared');
49948
50280
  }
50281
+ // ── Style bar & hover tracking ────────────────────────────────────────
50282
+ styleBarStyle = computed(() => {
50283
+ const rect = this.hoveredNodeRect();
50284
+ if (!rect) {
50285
+ return null;
50286
+ }
50287
+ return {
50288
+ position: 'absolute',
50289
+ left: `${Math.max(0, rect.left + rect.width - 120)}px`,
50290
+ top: `${Math.max(0, rect.top - 22)}px`,
50291
+ 'z-index': '25',
50292
+ };
50293
+ }, /* @ts-ignore */
50294
+ ...(ngDevMode ? [{ debugName: "styleBarStyle" }] : /* istanbul ignore next */ []));
50295
+ onMouseMove(event) {
50296
+ if (!this.canEditNodes()) {
50297
+ return;
50298
+ }
50299
+ const nodeEl = this.findNodeEl(event.target);
50300
+ const cnt = this.smartartContainer()?.nativeElement;
50301
+ const id = nodeEl?.getAttribute('data-smartart-node-id') ?? null;
50302
+ this.hoveredNodeId.set(id);
50303
+ this.hoveredNodeRect.set(id && nodeEl && cnt
50304
+ ? computeInlineEditorRect(nodeEl.getBoundingClientRect(), cnt.getBoundingClientRect())
50305
+ : null);
50306
+ }
50307
+ onMouseLeave() {
50308
+ this.hoveredNodeId.set(null);
50309
+ this.hoveredNodeRect.set(null);
50310
+ }
50311
+ findNodeEl(target) {
50312
+ let el = target instanceof Element ? target : null;
50313
+ while (el) {
50314
+ if (el.hasAttribute('data-smartart-node-id')) {
50315
+ return el;
50316
+ }
50317
+ el = el.parentElement;
50318
+ }
50319
+ return null;
50320
+ }
50321
+ handleChangeNodeStyle(nodeId, fill) {
50322
+ const data = this.smartArtData();
50323
+ if (!data || !this.editor) {
50324
+ return;
50325
+ }
50326
+ const next = setSmartArtNodeStyle(data, nodeId, { fillColor: fill });
50327
+ if (next === data) {
50328
+ return;
50329
+ }
50330
+ const slideIndex = findSlideIndexByElementId(this.editor.slides(), this.element().id);
50331
+ if (slideIndex < 0) {
50332
+ return;
50333
+ }
50334
+ this.editor.updateElement(slideIndex, this.element().id, {
50335
+ smartArtData: next,
50336
+ });
50337
+ }
49949
50338
  // ── Empty / no-data state ──────────────────────────────────────────────
49950
50339
  isEmpty = computed(() => this.nodes().length === 0 && !this.hasDrawingShapes(), /* @ts-ignore */
49951
50340
  ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
49952
50341
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SmartArtRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
49953
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SmartArtRendererComponent, isStandalone: true, selector: "pptx-smart-art-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "nodeEditor", first: true, predicate: ["nodeEditor"], descendants: true, isSignal: true }], ngImport: i0, template: `
50342
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SmartArtRendererComponent, isStandalone: true, selector: "pptx-smart-art-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "nodeEditor", first: true, predicate: ["nodeEditor"], descendants: true, isSignal: true }, { propertyName: "smartartContainer", first: true, predicate: ["smartartContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
49954
50343
  <div
49955
50344
  class="pptx-ng-element pptx-ng-smartart"
49956
50345
  [ngStyle]="containerStyle()"
49957
50346
  [attr.data-element-id]="element().id"
49958
50347
  >
49959
50348
  <div
50349
+ #smartartContainer
49960
50350
  class="pptx-ng-smartart-chrome"
49961
50351
  [ngStyle]="chromeStyle()"
49962
50352
  [attr.role]="a11y() ? a11y()!.role : null"
49963
50353
  [attr.aria-label]="a11y()?.label ?? null"
50354
+ (mousemove)="onMouseMove($event)"
50355
+ (mouseleave)="onMouseLeave()"
49964
50356
  >
49965
50357
  @if (isEmpty()) {
49966
50358
  <div class="pptx-ng-smartart-placeholder">SmartArt</div>
@@ -49970,8 +50362,14 @@ class SmartArtRendererComponent {
49970
50362
  [attr.viewBox]="svgViewBox()"
49971
50363
  preserveAspectRatio="xMidYMid meet"
49972
50364
  >
49973
- @for (shape of renderedShapes(); track shape.key) {
49974
- <g [ngStyle]="shadowFilter() ? { filter: shadowFilter() } : {}">
50365
+ @for (shape of renderedShapes(); track shape.key; let i = $index) {
50366
+ <g
50367
+ [ngStyle]="shadowFilter() ? { filter: shadowFilter() } : {}"
50368
+ [attr.data-smartart-node-id]="drawingShapeNodeIds()[i] ?? null"
50369
+ [class.pptx-ng-smartart-node--editable]="
50370
+ canEditNodes() && !!drawingShapeNodeIds()[i]
50371
+ "
50372
+ >
49975
50373
  @if (shape.isEllipse) {
49976
50374
  <ellipse
49977
50375
  [attr.cx]="shape.cx"
@@ -50037,6 +50435,7 @@ class SmartArtRendererComponent {
50037
50435
  [attr.tabindex]="canEditNodes() ? 0 : null"
50038
50436
  [attr.role]="canEditNodes() ? 'button' : 'img'"
50039
50437
  [attr.aria-label]="nodeAriaLabel(node) ?? node.text"
50438
+ [attr.data-smartart-node-id]="nodeKeyId(node)"
50040
50439
  (dblclick)="onNodeDblClick($event, node)"
50041
50440
  (keydown)="onNodeKeydown($event, node)"
50042
50441
  >
@@ -50118,6 +50517,25 @@ class SmartArtRendererComponent {
50118
50517
  <div class="pptx-ng-smartart-placeholder">SmartArt</div>
50119
50518
  }
50120
50519
 
50520
+ @if (canEditNodes() && hoveredNodeId() && !editState() && styleBarStyle()) {
50521
+ <div
50522
+ class="pptx-ng-smartart-style-bar"
50523
+ [ngStyle]="styleBarStyle()!"
50524
+ (mousedown)="$event.stopPropagation()"
50525
+ (click)="$event.stopPropagation()"
50526
+ >
50527
+ @for (color of palette().slice(0, 6); track color) {
50528
+ <button
50529
+ type="button"
50530
+ class="pptx-ng-smartart-swatch"
50531
+ [attr.aria-label]="'Set fill to ' + color"
50532
+ [style.background]="color"
50533
+ (click)="handleChangeNodeStyle(hoveredNodeId()!, color)"
50534
+ ></button>
50535
+ }
50536
+ </div>
50537
+ }
50538
+
50121
50539
  <!--
50122
50540
  Inline node-text editor. Positioned in element-local px (== viewBox
50123
50541
  units, since the SVG viewBox matches the element pixel size and the
@@ -50146,7 +50564,7 @@ class SmartArtRendererComponent {
50146
50564
  <span class="pptx-ng-sr-only" aria-live="polite" role="status">{{ liveMessage() }}</span>
50147
50565
  </div>
50148
50566
  </div>
50149
- `, isInline: true, styles: [".pptx-ng-smartart-chrome{box-sizing:border-box;overflow:hidden;position:relative}.pptx-ng-smartart-svg{width:100%;height:100%;pointer-events:none}.pptx-ng-smartart-node--editable{pointer-events:auto;cursor:text}.pptx-ng-smartart-node--editable:hover{filter:drop-shadow(0 0 2px rgba(96,165,250,.8))}.pptx-ng-smartart-node-editor{position:absolute;box-sizing:border-box;margin:0;padding:1px 2px;border:1px solid var(--pptx-inspector-active, #0078d4);border-radius:2px;background:#fff;color:#111;font-size:11px;line-height:1.1;text-align:center;resize:none;overflow:hidden;z-index:2}.pptx-ng-smartart-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:11px;color:#fffc;pointer-events:none}.pptx-ng-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
50567
+ `, isInline: true, styles: [".pptx-ng-smartart-chrome{box-sizing:border-box;overflow:hidden;position:relative}.pptx-ng-smartart-svg{width:100%;height:100%;pointer-events:none}.pptx-ng-smartart-node--editable{pointer-events:auto;cursor:text}.pptx-ng-smartart-node--editable:hover{filter:drop-shadow(0 0 2px rgba(96,165,250,.8))}.pptx-ng-smartart-node-editor{position:absolute;box-sizing:border-box;margin:0;padding:1px 2px;border:1px solid var(--pptx-inspector-active, #0078d4);border-radius:2px;background:#fff;color:#111;font-size:11px;line-height:1.1;text-align:center;resize:none;overflow:hidden;z-index:2}.pptx-ng-smartart-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:11px;color:#fffc;pointer-events:none}.pptx-ng-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.pptx-ng-smartart-style-bar{position:absolute;pointer-events:auto}.pptx-ng-smartart-swatch{width:14px;height:14px;border-radius:50%;border:1px solid rgba(0,0,0,.1);cursor:pointer;transition:transform .1s}.pptx-ng-smartart-swatch:hover{transform:scale(1.25)}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
50150
50568
  }
50151
50569
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SmartArtRendererComponent, decorators: [{
50152
50570
  type: Component,
@@ -50157,10 +50575,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
50157
50575
  [attr.data-element-id]="element().id"
50158
50576
  >
50159
50577
  <div
50578
+ #smartartContainer
50160
50579
  class="pptx-ng-smartart-chrome"
50161
50580
  [ngStyle]="chromeStyle()"
50162
50581
  [attr.role]="a11y() ? a11y()!.role : null"
50163
50582
  [attr.aria-label]="a11y()?.label ?? null"
50583
+ (mousemove)="onMouseMove($event)"
50584
+ (mouseleave)="onMouseLeave()"
50164
50585
  >
50165
50586
  @if (isEmpty()) {
50166
50587
  <div class="pptx-ng-smartart-placeholder">SmartArt</div>
@@ -50170,8 +50591,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
50170
50591
  [attr.viewBox]="svgViewBox()"
50171
50592
  preserveAspectRatio="xMidYMid meet"
50172
50593
  >
50173
- @for (shape of renderedShapes(); track shape.key) {
50174
- <g [ngStyle]="shadowFilter() ? { filter: shadowFilter() } : {}">
50594
+ @for (shape of renderedShapes(); track shape.key; let i = $index) {
50595
+ <g
50596
+ [ngStyle]="shadowFilter() ? { filter: shadowFilter() } : {}"
50597
+ [attr.data-smartart-node-id]="drawingShapeNodeIds()[i] ?? null"
50598
+ [class.pptx-ng-smartart-node--editable]="
50599
+ canEditNodes() && !!drawingShapeNodeIds()[i]
50600
+ "
50601
+ >
50175
50602
  @if (shape.isEllipse) {
50176
50603
  <ellipse
50177
50604
  [attr.cx]="shape.cx"
@@ -50237,6 +50664,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
50237
50664
  [attr.tabindex]="canEditNodes() ? 0 : null"
50238
50665
  [attr.role]="canEditNodes() ? 'button' : 'img'"
50239
50666
  [attr.aria-label]="nodeAriaLabel(node) ?? node.text"
50667
+ [attr.data-smartart-node-id]="nodeKeyId(node)"
50240
50668
  (dblclick)="onNodeDblClick($event, node)"
50241
50669
  (keydown)="onNodeKeydown($event, node)"
50242
50670
  >
@@ -50318,6 +50746,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
50318
50746
  <div class="pptx-ng-smartart-placeholder">SmartArt</div>
50319
50747
  }
50320
50748
 
50749
+ @if (canEditNodes() && hoveredNodeId() && !editState() && styleBarStyle()) {
50750
+ <div
50751
+ class="pptx-ng-smartart-style-bar"
50752
+ [ngStyle]="styleBarStyle()!"
50753
+ (mousedown)="$event.stopPropagation()"
50754
+ (click)="$event.stopPropagation()"
50755
+ >
50756
+ @for (color of palette().slice(0, 6); track color) {
50757
+ <button
50758
+ type="button"
50759
+ class="pptx-ng-smartart-swatch"
50760
+ [attr.aria-label]="'Set fill to ' + color"
50761
+ [style.background]="color"
50762
+ (click)="handleChangeNodeStyle(hoveredNodeId()!, color)"
50763
+ ></button>
50764
+ }
50765
+ </div>
50766
+ }
50767
+
50321
50768
  <!--
50322
50769
  Inline node-text editor. Positioned in element-local px (== viewBox
50323
50770
  units, since the SVG viewBox matches the element pixel size and the
@@ -50346,8 +50793,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
50346
50793
  <span class="pptx-ng-sr-only" aria-live="polite" role="status">{{ liveMessage() }}</span>
50347
50794
  </div>
50348
50795
  </div>
50349
- `, styles: [".pptx-ng-smartart-chrome{box-sizing:border-box;overflow:hidden;position:relative}.pptx-ng-smartart-svg{width:100%;height:100%;pointer-events:none}.pptx-ng-smartart-node--editable{pointer-events:auto;cursor:text}.pptx-ng-smartart-node--editable:hover{filter:drop-shadow(0 0 2px rgba(96,165,250,.8))}.pptx-ng-smartart-node-editor{position:absolute;box-sizing:border-box;margin:0;padding:1px 2px;border:1px solid var(--pptx-inspector-active, #0078d4);border-radius:2px;background:#fff;color:#111;font-size:11px;line-height:1.1;text-align:center;resize:none;overflow:hidden;z-index:2}.pptx-ng-smartart-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:11px;color:#fffc;pointer-events:none}.pptx-ng-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
50350
- }], ctorParameters: () => [], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], nodeEditor: [{ type: i0.ViewChild, args: ['nodeEditor', { isSignal: true }] }] } });
50796
+ `, styles: [".pptx-ng-smartart-chrome{box-sizing:border-box;overflow:hidden;position:relative}.pptx-ng-smartart-svg{width:100%;height:100%;pointer-events:none}.pptx-ng-smartart-node--editable{pointer-events:auto;cursor:text}.pptx-ng-smartart-node--editable:hover{filter:drop-shadow(0 0 2px rgba(96,165,250,.8))}.pptx-ng-smartart-node-editor{position:absolute;box-sizing:border-box;margin:0;padding:1px 2px;border:1px solid var(--pptx-inspector-active, #0078d4);border-radius:2px;background:#fff;color:#111;font-size:11px;line-height:1.1;text-align:center;resize:none;overflow:hidden;z-index:2}.pptx-ng-smartart-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:11px;color:#fffc;pointer-events:none}.pptx-ng-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.pptx-ng-smartart-style-bar{position:absolute;pointer-events:auto}.pptx-ng-smartart-swatch{width:14px;height:14px;border-radius:50%;border:1px solid rgba(0,0,0,.1);cursor:pointer;transition:transform .1s}.pptx-ng-smartart-swatch:hover{transform:scale(1.25)}\n"] }]
50797
+ }], ctorParameters: () => [], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], nodeEditor: [{ type: i0.ViewChild, args: ['nodeEditor', { isSignal: true }] }], smartartContainer: [{ type: i0.ViewChild, args: ['smartartContainer', { isSignal: true }] }] } });
50351
50798
 
50352
50799
  const PALETTES = {
50353
50800
  colorful1: ['#3b82f6', '#22c55e', '#f97316', '#eab308', '#a855f7', '#ec4899'],
@@ -50364,20 +50811,41 @@ const PALETTES = {
50364
50811
  * `pptx-viewer-shared/smartart-3d` and mounts it on a canvas. `three` is an
50365
50812
  * optional peer dependency: when it is missing, the diagram has no geometry, or
50366
50813
  * the scene errors, the component falls back to the SVG SmartArt renderer.
50814
+ *
50815
+ * When `canEdit` is true and the 3D scene is active, an invisible
50816
+ * `<pptx-smart-art-renderer>` overlay is stacked over the canvas. Double-clicking
50817
+ * on the overlay uses `document.elementsFromPoint` to locate the `<g>` bearing
50818
+ * `data-smartart-node-id`, then opens an inline textarea editor over that node
50819
+ * (same commit path as the SVG renderer: `EditorStateService.updateElement`).
50367
50820
  */
50368
50821
  class SmartArt3DRendererComponent {
50369
50822
  element = input.required(/* @ts-ignore */
50370
50823
  ...(ngDevMode ? [{ debugName: "element" }] : /* istanbul ignore next */ []));
50371
50824
  zIndex = input(0, /* @ts-ignore */
50372
50825
  ...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
50826
+ /** When true and the 3D scene is active, enables inline node text editing. */
50827
+ canEdit = input(false, /* @ts-ignore */
50828
+ ...(ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []));
50373
50829
  canvas = viewChild('canvas', /* @ts-ignore */
50374
50830
  ...(ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []));
50831
+ containerEl = viewChild('container3d', /* @ts-ignore */
50832
+ ...(ngDevMode ? [{ debugName: "containerEl" }] : /* istanbul ignore next */ []));
50833
+ nodeEditor3d = viewChild('nodeEditor3d', /* @ts-ignore */
50834
+ ...(ngDevMode ? [{ debugName: "nodeEditor3d" }] : /* istanbul ignore next */ []));
50375
50835
  /** `true` until the 3D scene is known to be mountable; renders the SVG fallback. */
50376
50836
  useFallback = signal(true, /* @ts-ignore */
50377
50837
  ...(ngDevMode ? [{ debugName: "useFallback" }] : /* istanbul ignore next */ []));
50378
50838
  mountFn = signal(null, /* @ts-ignore */
50379
50839
  ...(ngDevMode ? [{ debugName: "mountFn" }] : /* istanbul ignore next */ []));
50380
50840
  handle = null;
50841
+ editState = signal(null, /* @ts-ignore */
50842
+ ...(ngDevMode ? [{ debugName: "editState" }] : /* istanbul ignore next */ []));
50843
+ /** Live draft text, updated on every input event. */
50844
+ draftText = '';
50845
+ /** Guards against a cancel-triggered DOM-removal blur committing the edit. */
50846
+ editSettled = false;
50847
+ editor = inject(EditorStateService, { optional: true });
50848
+ injector = inject(Injector);
50381
50849
  containerStyle = computed(() => getContainerStyle(this.element(), this.zIndex()), /* @ts-ignore */
50382
50850
  ...(ngDevMode ? [{ debugName: "containerStyle" }] : /* istanbul ignore next */ []));
50383
50851
  smartArtData = computed(() => {
@@ -50426,6 +50894,18 @@ class SmartArt3DRendererComponent {
50426
50894
  const el = this.element();
50427
50895
  this.handle?.resize(el.width, el.height);
50428
50896
  });
50897
+ // Auto-focus the textarea when the editor opens.
50898
+ effect(() => {
50899
+ if (this.editState()) {
50900
+ afterNextRender(() => {
50901
+ const el = this.nodeEditor3d()?.nativeElement;
50902
+ if (el) {
50903
+ el.focus();
50904
+ el.select();
50905
+ }
50906
+ }, { injector: this.injector });
50907
+ }
50908
+ });
50429
50909
  }
50430
50910
  async loadScene() {
50431
50911
  const m = this.model();
@@ -50433,7 +50913,7 @@ class SmartArt3DRendererComponent {
50433
50913
  return; // No geometry: stay on the SVG fallback.
50434
50914
  }
50435
50915
  try {
50436
- const mod = await import('./pptx-angular-viewer-index-BfKeZbQy.mjs');
50916
+ const mod = await import('./pptx-angular-viewer-index-BbiIOY_Y.mjs');
50437
50917
  this.mountFn.set(mod.mountSmartArt3D);
50438
50918
  this.useFallback.set(false);
50439
50919
  }
@@ -50441,24 +50921,144 @@ class SmartArt3DRendererComponent {
50441
50921
  this.useFallback.set(true);
50442
50922
  }
50443
50923
  }
50924
+ /**
50925
+ * Locate the SmartArt node at the click position using `elementsFromPoint`
50926
+ * (which includes pointer-events:none SVG elements) and open the inline editor.
50927
+ */
50928
+ onOverlayDblClick(event) {
50929
+ const container = this.containerEl()?.nativeElement;
50930
+ if (!container) {
50931
+ return;
50932
+ }
50933
+ const data = this.smartArtData();
50934
+ if (!data) {
50935
+ return;
50936
+ }
50937
+ // document.elementsFromPoint includes elements with pointer-events:none,
50938
+ // so we can find the <g data-smartart-node-id="..."> in the overlay SVG.
50939
+ const elements = document.elementsFromPoint(event.clientX, event.clientY);
50940
+ const nodeEl = elements.find((el) => el instanceof Element && el.hasAttribute('data-smartart-node-id'));
50941
+ if (!nodeEl) {
50942
+ return;
50943
+ }
50944
+ const nodeId = nodeEl.getAttribute('data-smartart-node-id');
50945
+ if (!nodeId) {
50946
+ return;
50947
+ }
50948
+ const currentText = data.nodes.find((n) => n.id === nodeId)?.text ?? '';
50949
+ const nodeRect = nodeEl.getBoundingClientRect();
50950
+ const containerRect = container.getBoundingClientRect();
50951
+ this.draftText = currentText;
50952
+ this.editSettled = false;
50953
+ this.editState.set({
50954
+ nodeId,
50955
+ box: {
50956
+ x: nodeRect.left - containerRect.left,
50957
+ y: nodeRect.top - containerRect.top,
50958
+ width: nodeRect.width,
50959
+ height: nodeRect.height,
50960
+ },
50961
+ text: currentText,
50962
+ });
50963
+ }
50964
+ /** Update the live draft text on each keystroke. */
50965
+ updateDraft(event) {
50966
+ this.draftText = event.target.value;
50967
+ }
50968
+ /** Enter commits (via blur); Escape cancels. Propagation always stopped. */
50969
+ onEditorKeydown(event) {
50970
+ event.stopPropagation();
50971
+ if (event.key === 'Enter' && !event.shiftKey) {
50972
+ event.preventDefault();
50973
+ // Commit via blur so the single commit path runs once.
50974
+ event.target.blur();
50975
+ }
50976
+ else if (event.key === 'Escape') {
50977
+ event.preventDefault();
50978
+ this.cancelEdit();
50979
+ }
50980
+ }
50981
+ /** Commit the current draft through EditorStateService (blur handler). */
50982
+ commitEdit() {
50983
+ if (this.editSettled) {
50984
+ this.editSettled = false;
50985
+ return;
50986
+ }
50987
+ const edit = this.editState();
50988
+ if (!edit) {
50989
+ return;
50990
+ }
50991
+ const text = this.draftText;
50992
+ this.editState.set(null);
50993
+ this.applyCommit(edit.nodeId, text);
50994
+ }
50995
+ /** Discard the current edit without committing. */
50996
+ cancelEdit() {
50997
+ // Mark as settled so the DOM-removal blur does not commit the cancelled edit.
50998
+ this.editSettled = true;
50999
+ this.editState.set(null);
51000
+ }
51001
+ applyCommit(nodeId, text) {
51002
+ const data = this.smartArtData();
51003
+ if (!data || !this.editor) {
51004
+ return;
51005
+ }
51006
+ const next = commitNodeText(data, nodeId, text);
51007
+ if (next === data) {
51008
+ return;
51009
+ } // no-op: text unchanged
51010
+ const slideIndex = findSlideIndexByElementId(this.editor.slides(), this.element().id);
51011
+ if (slideIndex < 0) {
51012
+ return;
51013
+ }
51014
+ this.editor.updateElement(slideIndex, this.element().id, {
51015
+ smartArtData: next,
51016
+ });
51017
+ }
50444
51018
  ngOnDestroy() {
50445
51019
  this.handle?.dispose();
50446
51020
  this.handle = null;
50447
51021
  }
50448
51022
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SmartArt3DRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
50449
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SmartArt3DRendererComponent, isStandalone: true, selector: "pptx-smart-art-3d-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: `
51023
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SmartArt3DRendererComponent, isStandalone: true, selector: "pptx-smart-art-3d-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, canEdit: { classPropertyName: "canEdit", publicName: "canEdit", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }, { propertyName: "containerEl", first: true, predicate: ["container3d"], descendants: true, isSignal: true }, { propertyName: "nodeEditor3d", first: true, predicate: ["nodeEditor3d"], descendants: true, isSignal: true }], ngImport: i0, template: `
50450
51024
  @if (useFallback()) {
50451
51025
  <pptx-smart-art-renderer [element]="element()" [zIndex]="zIndex()" />
50452
51026
  } @else {
50453
51027
  <div
51028
+ #container3d
50454
51029
  class="pptx-ng-element pptx-ng-smartart-3d"
50455
51030
  [ngStyle]="containerStyle()"
50456
51031
  [attr.data-element-id]="element().id"
50457
51032
  >
50458
51033
  <canvas #canvas class="pptx-ng-smartart-3d-canvas"></canvas>
51034
+ @if (canEdit()) {
51035
+ <!-- Invisible SVG overlay: provides data-smartart-node-id hit targets -->
51036
+ <div class="pptx-ng-smartart-3d-hittest" (dblclick)="onOverlayDblClick($event)">
51037
+ <pptx-smart-art-renderer [element]="element()" [editable]="false" [zIndex]="0" />
51038
+ </div>
51039
+ @if (editState()) {
51040
+ <textarea
51041
+ #nodeEditor3d
51042
+ class="pptx-ng-smartart-3d-node-editor"
51043
+ [style.left.px]="editState()!.box.x"
51044
+ [style.top.px]="editState()!.box.y"
51045
+ [style.width.px]="editState()!.box.width"
51046
+ [style.height.px]="editState()!.box.height"
51047
+ [value]="editState()!.text"
51048
+ spellcheck="false"
51049
+ aria-label="Edit SmartArt node text"
51050
+ (input)="updateDraft($event)"
51051
+ (blur)="commitEdit()"
51052
+ (keydown)="onEditorKeydown($event)"
51053
+ (mousedown)="$event.stopPropagation()"
51054
+ (click)="$event.stopPropagation()"
51055
+ (dblclick)="$event.stopPropagation()"
51056
+ ></textarea>
51057
+ }
51058
+ }
50459
51059
  </div>
50460
51060
  }
50461
- `, isInline: true, styles: [".pptx-ng-smartart-3d-canvas{width:100%;height:100%;display:block}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SmartArtRendererComponent, selector: "pptx-smart-art-renderer", inputs: ["element", "zIndex", "editable"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
51061
+ `, isInline: true, styles: [".pptx-ng-smartart-3d-canvas{width:100%;height:100%;display:block}.pptx-ng-smartart-3d-hittest{position:absolute;inset:0;opacity:0;pointer-events:auto}.pptx-ng-smartart-3d-node-editor{position:absolute;box-sizing:border-box;margin:0;padding:1px 2px;border:1px solid var(--pptx-inspector-active, #0078d4);border-radius:2px;background:#fff;color:#111;font-size:11px;line-height:1.1;text-align:center;resize:none;overflow:hidden;z-index:20;outline:none}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SmartArtRendererComponent, selector: "pptx-smart-art-renderer", inputs: ["element", "zIndex", "editable"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
50462
51062
  }
50463
51063
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SmartArt3DRendererComponent, decorators: [{
50464
51064
  type: Component,
@@ -50467,15 +51067,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
50467
51067
  <pptx-smart-art-renderer [element]="element()" [zIndex]="zIndex()" />
50468
51068
  } @else {
50469
51069
  <div
51070
+ #container3d
50470
51071
  class="pptx-ng-element pptx-ng-smartart-3d"
50471
51072
  [ngStyle]="containerStyle()"
50472
51073
  [attr.data-element-id]="element().id"
50473
51074
  >
50474
51075
  <canvas #canvas class="pptx-ng-smartart-3d-canvas"></canvas>
51076
+ @if (canEdit()) {
51077
+ <!-- Invisible SVG overlay: provides data-smartart-node-id hit targets -->
51078
+ <div class="pptx-ng-smartart-3d-hittest" (dblclick)="onOverlayDblClick($event)">
51079
+ <pptx-smart-art-renderer [element]="element()" [editable]="false" [zIndex]="0" />
51080
+ </div>
51081
+ @if (editState()) {
51082
+ <textarea
51083
+ #nodeEditor3d
51084
+ class="pptx-ng-smartart-3d-node-editor"
51085
+ [style.left.px]="editState()!.box.x"
51086
+ [style.top.px]="editState()!.box.y"
51087
+ [style.width.px]="editState()!.box.width"
51088
+ [style.height.px]="editState()!.box.height"
51089
+ [value]="editState()!.text"
51090
+ spellcheck="false"
51091
+ aria-label="Edit SmartArt node text"
51092
+ (input)="updateDraft($event)"
51093
+ (blur)="commitEdit()"
51094
+ (keydown)="onEditorKeydown($event)"
51095
+ (mousedown)="$event.stopPropagation()"
51096
+ (click)="$event.stopPropagation()"
51097
+ (dblclick)="$event.stopPropagation()"
51098
+ ></textarea>
51099
+ }
51100
+ }
50475
51101
  </div>
50476
51102
  }
50477
- `, styles: [".pptx-ng-smartart-3d-canvas{width:100%;height:100%;display:block}\n"] }]
50478
- }], ctorParameters: () => [], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }] } });
51103
+ `, styles: [".pptx-ng-smartart-3d-canvas{width:100%;height:100%;display:block}.pptx-ng-smartart-3d-hittest{position:absolute;inset:0;opacity:0;pointer-events:auto}.pptx-ng-smartart-3d-node-editor{position:absolute;box-sizing:border-box;margin:0;padding:1px 2px;border:1px solid var(--pptx-inspector-active, #0078d4);border-radius:2px;background:#fff;color:#111;font-size:11px;line-height:1.1;text-align:center;resize:none;overflow:hidden;z-index:20;outline:none}\n"] }]
51104
+ }], ctorParameters: () => [], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], canEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "canEdit", required: false }] }], canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], containerEl: [{ type: i0.ViewChild, args: ['container3d', { isSignal: true }] }], nodeEditor3d: [{ type: i0.ViewChild, args: ['nodeEditor3d', { isSignal: true }] }] } });
50479
51105
 
50480
51106
  /**
50481
51107
  * Opt-in flag for the Three.js SmartArt renderer (Angular).
@@ -51234,19 +51860,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
51234
51860
  type: Injectable
51235
51861
  }] });
51236
51862
 
51863
+ /** Fallback tile background when the target slide has no background colour. */
51864
+ const FALLBACK_THUMBNAIL_BG = '#f0f0f0';
51237
51865
  /**
51238
51866
  * Build the complete zoom view-model for a given element.
51867
+ *
51868
+ * When `targetInfo` is supplied (the target slide was resolved from the deck),
51869
+ * the fallback-thumbnail fields use the real target slide: its `backgroundColor`
51870
+ * as the tile background, `Slide ${slideNumber}` as the label, and its friendly
51871
+ * `sectionName` as the caption. This mirrors React's `ZoomSlideThumbnail`. When
51872
+ * it is absent the old fallback applies: grey background, `Slide ${index + 1}`,
51873
+ * and the raw `targetSectionId`.
51874
+ *
51239
51875
  * Returns sensible defaults for non-zoom elements (all derived strings are
51240
51876
  * empty/fallback values, `zoom` is `undefined`).
51241
51877
  */
51242
- function buildZoomViewModel(element) {
51878
+ function buildZoomViewModel(element, targetInfo) {
51243
51879
  const zoom = isZoomElement(element) ? element : undefined;
51244
51880
  const previewSrc = zoom?.imageData;
51245
51881
  const targetSlideIndex = zoom?.targetSlideIndex ?? 0;
51246
51882
  const zoomType = zoom?.zoomType ?? 'slide';
51247
51883
  const targetSectionId = zoom?.targetSectionId;
51248
51884
  const badgeText = zoomType === 'section' ? 'Section Zoom' : 'Slide Zoom';
51249
- const slideLabel = `Slide ${targetSlideIndex + 1}`;
51885
+ const slideLabel = targetInfo?.slideNumber !== undefined
51886
+ ? `Slide ${targetInfo.slideNumber}`
51887
+ : `Slide ${targetSlideIndex + 1}`;
51888
+ const thumbnailBackground = targetInfo?.backgroundColor ?? FALLBACK_THUMBNAIL_BG;
51889
+ const sectionCaption = targetInfo?.sectionName ?? targetSectionId;
51250
51890
  let ariaLabel = `Zoom to slide ${targetSlideIndex + 1}`;
51251
51891
  if (zoomType === 'section' && targetSectionId) {
51252
51892
  ariaLabel = `${ariaLabel} (section: ${targetSectionId})`;
@@ -51260,8 +51900,17 @@ function buildZoomViewModel(element) {
51260
51900
  badgeText,
51261
51901
  slideLabel,
51262
51902
  ariaLabel,
51903
+ thumbnailBackground,
51904
+ sectionCaption,
51263
51905
  };
51264
51906
  }
51907
+ /**
51908
+ * Resolve a zoom element's zero-based target slide index, or `0` for non-zoom
51909
+ * elements. Used to look the target slide up before building the view model.
51910
+ */
51911
+ function zoomTargetSlideIndex(element) {
51912
+ return isZoomElement(element) ? element.targetSlideIndex : 0;
51913
+ }
51265
51914
  /** Wrapper `[ngStyle]`-compatible style for the zoom container `<div>`. */
51266
51915
  function buildZoomContainerStyle(element, zIndex) {
51267
51916
  return getContainerStyle(element, zIndex);
@@ -51274,6 +51923,49 @@ function isZoomActivationKey(key) {
51274
51923
  return key === 'Enter' || key === ' ';
51275
51924
  }
51276
51925
 
51926
+ /**
51927
+ * ZoomTargetService: viewer-scoped lookup from a zoom element's target slide
51928
+ * index to a {@link ZoomTargetInfo} descriptor.
51929
+ *
51930
+ * Provided by `PowerPointViewerComponent` from its loaded slides, then injected
51931
+ * `{ optional: true }` by `ZoomRendererComponent`. Trees that do not provide it
51932
+ * (e.g. isolated component tests) resolve `null`, so the renderer falls back to
51933
+ * the old grey/index/GUID thumbnail. This mirrors the optional-DI pattern of
51934
+ * {@link ZoomNavigationService}.
51935
+ *
51936
+ * Intentionally NOT `providedIn: 'root'`: it is supplied per viewer so the
51937
+ * lookup always reflects that viewer's deck.
51938
+ */
51939
+ class ZoomTargetService {
51940
+ /** The deck the lookup resolves against; seeded by the viewer. */
51941
+ slides = signal([], /* @ts-ignore */
51942
+ ...(ngDevMode ? [{ debugName: "slides" }] : /* istanbul ignore next */ []));
51943
+ /** Replace the deck used to resolve zoom targets. */
51944
+ setSlides(slides) {
51945
+ this.slides.set(slides);
51946
+ }
51947
+ /**
51948
+ * Resolve the descriptor for a zoom's target slide, or `undefined` when the
51949
+ * index is out of range (so the renderer keeps its index-based fallback).
51950
+ */
51951
+ lookup(targetSlideIndex) {
51952
+ const slide = this.slides()[targetSlideIndex];
51953
+ if (!slide) {
51954
+ return undefined;
51955
+ }
51956
+ return {
51957
+ backgroundColor: slide.backgroundColor,
51958
+ slideNumber: slide.slideNumber,
51959
+ sectionName: slide.sectionName,
51960
+ };
51961
+ }
51962
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ZoomTargetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
51963
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ZoomTargetService });
51964
+ }
51965
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ZoomTargetService, decorators: [{
51966
+ type: Injectable
51967
+ }] });
51968
+
51277
51969
  /**
51278
51970
  * ZoomRendererComponent: Angular port of the Vue `ZoomRenderer.vue`
51279
51971
  * (and the React `ZoomElementRenderer`), static viewer-first subset.
@@ -51286,9 +51978,15 @@ function isZoomActivationKey(key) {
51286
51978
  * In presentation mode the overlay provides a {@link ZoomNavigationService}, so
51287
51979
  * clicking (or Enter/Space) jumps to the target slide. Outside presentation mode
51288
51980
  * the service is not provided (optional injection yields `null`) and the tile
51289
- * stays a static link, exactly as before. Live target-slide preview rendering is
51290
- * still NOT ported; the `slides` array is not threaded through, so the fallback
51291
- * uses the target slide index rather than the real target background.
51981
+ * stays a static link, exactly as before.
51982
+ *
51983
+ * The fallback thumbnail (no embedded preview image) matches React's
51984
+ * `ZoomSlideThumbnail`: when a {@link ZoomTargetService} is provided (by the
51985
+ * viewer), it looks up the target slide and uses that slide's real background
51986
+ * colour, its own 1-based number, and its friendly section name. A live
51987
+ * mini-rendering of the target slide is intentionally NOT drawn. When the
51988
+ * service is absent the tile keeps the neutral grey / index / section-GUID
51989
+ * fallback.
51292
51990
  *
51293
51991
  * All non-trivial pure computation lives in `zoom-renderer-helpers.ts` (no
51294
51992
  * Angular dependency) so it can be unit-tested without TestBed.
@@ -51302,7 +52000,17 @@ class ZoomRendererComponent {
51302
52000
  ...(ngDevMode ? [{ debugName: "mediaDataUrls" }] : /* istanbul ignore next */ []));
51303
52001
  containerStyle = computed(() => buildZoomContainerStyle(this.element(), this.zIndex()), /* @ts-ignore */
51304
52002
  ...(ngDevMode ? [{ debugName: "containerStyle" }] : /* istanbul ignore next */ []));
51305
- vm = computed(() => buildZoomViewModel(this.element()), /* @ts-ignore */
52003
+ /**
52004
+ * Target-slide lookup, provided by the viewer. `null` in trees that do not
52005
+ * provide it (e.g. isolated component tests), where the fallback thumbnail
52006
+ * stays on the neutral grey / index / section-GUID placeholder.
52007
+ */
52008
+ zoomTarget = inject(ZoomTargetService, { optional: true });
52009
+ vm = computed(() => {
52010
+ const element = this.element();
52011
+ const targetSlideIndex = zoomTargetSlideIndex(element);
52012
+ return buildZoomViewModel(element, this.zoomTarget?.lookup(targetSlideIndex));
52013
+ }, /* @ts-ignore */
51306
52014
  ...(ngDevMode ? [{ debugName: "vm" }] : /* istanbul ignore next */ []));
51307
52015
  /**
51308
52016
  * Zoom-navigation context, present only inside a running presentation (the
@@ -51364,13 +52072,14 @@ class ZoomRendererComponent {
51364
52072
  />
51365
52073
  } @else {
51366
52074
  <div
51367
- style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background-color:#f0f0f0;border:1px solid rgba(0,0,0,0.1);box-sizing:border-box"
52075
+ style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;border:1px solid rgba(0,0,0,0.1);box-sizing:border-box"
52076
+ [style.background-color]="vm().thumbnailBackground"
51368
52077
  >
51369
52078
  <div style="font-size:14px;font-weight:600;color:rgba(0,0,0,0.5);margin-bottom:4px">
51370
52079
  {{ vm().slideLabel }}
51371
52080
  </div>
51372
- @if (vm().targetSectionId) {
51373
- <div style="font-size:10px;color:rgba(0,0,0,0.4)">{{ vm().targetSectionId }}</div>
52081
+ @if (vm().sectionCaption) {
52082
+ <div style="font-size:10px;color:rgba(0,0,0,0.4)">{{ vm().sectionCaption }}</div>
51374
52083
  }
51375
52084
  </div>
51376
52085
  }
@@ -51412,13 +52121,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
51412
52121
  />
51413
52122
  } @else {
51414
52123
  <div
51415
- style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background-color:#f0f0f0;border:1px solid rgba(0,0,0,0.1);box-sizing:border-box"
52124
+ style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;border:1px solid rgba(0,0,0,0.1);box-sizing:border-box"
52125
+ [style.background-color]="vm().thumbnailBackground"
51416
52126
  >
51417
52127
  <div style="font-size:14px;font-weight:600;color:rgba(0,0,0,0.5);margin-bottom:4px">
51418
52128
  {{ vm().slideLabel }}
51419
52129
  </div>
51420
- @if (vm().targetSectionId) {
51421
- <div style="font-size:10px;color:rgba(0,0,0,0.4)">{{ vm().targetSectionId }}</div>
52130
+ @if (vm().sectionCaption) {
52131
+ <div style="font-size:10px;color:rgba(0,0,0,0.4)">{{ vm().sectionCaption }}</div>
51422
52132
  }
51423
52133
  </div>
51424
52134
  }
@@ -51741,7 +52451,11 @@ class ElementRendererComponent {
51741
52451
  />
51742
52452
  }
51743
52453
  @case (element().type === 'smartArt' && smartArt3D()) {
51744
- <pptx-smart-art-3d-renderer [element]="element()" [zIndex]="zIndex()" />
52454
+ <pptx-smart-art-3d-renderer
52455
+ [element]="element()"
52456
+ [zIndex]="zIndex()"
52457
+ [canEdit]="interactive() && editable()"
52458
+ />
51745
52459
  }
51746
52460
  @case (element().type === 'smartArt') {
51747
52461
  <div
@@ -51982,7 +52696,7 @@ class ElementRendererComponent {
51982
52696
  </defs>
51983
52697
  </svg>
51984
52698
  }
51985
- `, isInline: true, dependencies: [{ kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex", "obstacles", "canvasWidth", "canvasHeight", "interactive", "editable", "fieldContext", "editTemplateMode"], outputs: ["cellCommit"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ConnectorRendererComponent, selector: "pptx-connector-renderer", inputs: ["element", "zIndex", "obstacles", "canvasWidth", "canvasHeight", "interactive"] }, { kind: "component", type: TableRendererComponent, selector: "pptx-table-renderer", inputs: ["element", "editable"], outputs: ["cellCommit"] }, { kind: "component", type: ChartRendererComponent, selector: "pptx-chart-renderer", inputs: ["element"] }, { kind: "component", type: SmartArtRendererComponent, selector: "pptx-smart-art-renderer", inputs: ["element", "zIndex", "editable"] }, { kind: "component", type: SmartArt3DRendererComponent, selector: "pptx-smart-art-3d-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: InkRendererComponent, selector: "pptx-ink-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: OleRendererComponent, selector: "pptx-ole-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: Model3DRendererComponent, selector: "pptx-model3d-renderer", inputs: ["element", "zIndex", "mediaDataUrls", "interactive"] }, { kind: "component", type: ZoomRendererComponent, selector: "pptx-zoom-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: EquationRendererComponent, selector: "pptx-equation-renderer", inputs: ["equationXml", "equationNumber"] }, { kind: "component", type: ColorChangedImageComponent, selector: "pptx-color-changed-image", inputs: ["src", "clrChange", "alt", "imgClass", "imgStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
52699
+ `, isInline: true, dependencies: [{ kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex", "obstacles", "canvasWidth", "canvasHeight", "interactive", "editable", "fieldContext", "editTemplateMode"], outputs: ["cellCommit"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ConnectorRendererComponent, selector: "pptx-connector-renderer", inputs: ["element", "zIndex", "obstacles", "canvasWidth", "canvasHeight", "interactive"] }, { kind: "component", type: TableRendererComponent, selector: "pptx-table-renderer", inputs: ["element", "editable"], outputs: ["cellCommit"] }, { kind: "component", type: ChartRendererComponent, selector: "pptx-chart-renderer", inputs: ["element"] }, { kind: "component", type: SmartArtRendererComponent, selector: "pptx-smart-art-renderer", inputs: ["element", "zIndex", "editable"] }, { kind: "component", type: SmartArt3DRendererComponent, selector: "pptx-smart-art-3d-renderer", inputs: ["element", "zIndex", "canEdit"] }, { kind: "component", type: InkRendererComponent, selector: "pptx-ink-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: OleRendererComponent, selector: "pptx-ole-renderer", inputs: ["element", "zIndex"] }, { kind: "component", type: Model3DRendererComponent, selector: "pptx-model3d-renderer", inputs: ["element", "zIndex", "mediaDataUrls", "interactive"] }, { kind: "component", type: ZoomRendererComponent, selector: "pptx-zoom-renderer", inputs: ["element", "zIndex", "mediaDataUrls"] }, { kind: "component", type: EquationRendererComponent, selector: "pptx-equation-renderer", inputs: ["equationXml", "equationNumber"] }, { kind: "component", type: ColorChangedImageComponent, selector: "pptx-color-changed-image", inputs: ["src", "clrChange", "alt", "imgClass", "imgStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
51986
52700
  }
51987
52701
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ElementRendererComponent, decorators: [{
51988
52702
  type: Component,
@@ -52038,7 +52752,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
52038
52752
  />
52039
52753
  }
52040
52754
  @case (element().type === 'smartArt' && smartArt3D()) {
52041
- <pptx-smart-art-3d-renderer [element]="element()" [zIndex]="zIndex()" />
52755
+ <pptx-smart-art-3d-renderer
52756
+ [element]="element()"
52757
+ [zIndex]="zIndex()"
52758
+ [canEdit]="interactive() && editable()"
52759
+ />
52042
52760
  }
52043
52761
  @case (element().type === 'smartArt') {
52044
52762
  <div
@@ -58974,6 +59692,9 @@ class RibbonComponent {
58974
59692
  hasSel() {
58975
59693
  return this.editor.selectedIds().length > 0;
58976
59694
  }
59695
+ canDistribute() {
59696
+ return this.editor.selectedIds().length >= 3;
59697
+ }
58977
59698
  isText() {
58978
59699
  const el = this.selectedElement();
58979
59700
  return el !== null && hasTextProperties(el);
@@ -59577,6 +60298,28 @@ class RibbonComponent {
59577
60298
  </button>
59578
60299
  </div>
59579
60300
  <span class="pptx-rb-sep"></span>
60301
+ <!-- Distribute -->
60302
+ <div class="pptx-rb-grp">
60303
+ <button
60304
+ type="button"
60305
+ class="pptx-rb-gb"
60306
+ [disabled]="!canDistribute()"
60307
+ title="Distribute horizontally"
60308
+ (click)="editor.distributeSelected(slideIndex(), 'horizontal')"
60309
+ >
60310
+ &#x2194; H
60311
+ </button>
60312
+ <button
60313
+ type="button"
60314
+ class="pptx-rb-gl"
60315
+ [disabled]="!canDistribute()"
60316
+ title="Distribute vertically"
60317
+ (click)="editor.distributeSelected(slideIndex(), 'vertical')"
60318
+ >
60319
+ &#x2195; V
60320
+ </button>
60321
+ </div>
60322
+ <span class="pptx-rb-sep"></span>
59580
60323
  <!-- Group / edit -->
59581
60324
  <div class="pptx-rb-grp">
59582
60325
  <button
@@ -60557,6 +61300,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
60557
61300
  </button>
60558
61301
  </div>
60559
61302
  <span class="pptx-rb-sep"></span>
61303
+ <!-- Distribute -->
61304
+ <div class="pptx-rb-grp">
61305
+ <button
61306
+ type="button"
61307
+ class="pptx-rb-gb"
61308
+ [disabled]="!canDistribute()"
61309
+ title="Distribute horizontally"
61310
+ (click)="editor.distributeSelected(slideIndex(), 'horizontal')"
61311
+ >
61312
+ &#x2194; H
61313
+ </button>
61314
+ <button
61315
+ type="button"
61316
+ class="pptx-rb-gl"
61317
+ [disabled]="!canDistribute()"
61318
+ title="Distribute vertically"
61319
+ (click)="editor.distributeSelected(slideIndex(), 'vertical')"
61320
+ >
61321
+ &#x2195; V
61322
+ </button>
61323
+ </div>
61324
+ <span class="pptx-rb-sep"></span>
60560
61325
  <!-- Group / edit -->
60561
61326
  <div class="pptx-rb-grp">
60562
61327
  <button
@@ -62743,6 +63508,7 @@ class PowerPointViewerComponent {
62743
63508
  print = inject(PrintService);
62744
63509
  mobile = inject(IsMobileService);
62745
63510
  smartArt3DSvc = inject(SmartArt3DService);
63511
+ zoomTarget = inject(ZoomTargetService);
62746
63512
  /** The `<main>` host; used to locate the live `.pptx-ng-canvas-stage`. */
62747
63513
  mainEl = viewChild('mainEl', /* @ts-ignore */
62748
63514
  ...(ngDevMode ? [{ debugName: "mainEl" }] : /* istanbul ignore next */ []));
@@ -63121,6 +63887,12 @@ class PowerPointViewerComponent {
63121
63887
  effect(() => {
63122
63888
  this.accessibility.setSlides([...this.mergedSlides()]);
63123
63889
  });
63890
+ // Feed the deck to the zoom-target lookup so a zoom tile's fallback
63891
+ // thumbnail can resolve its target slide's background / number / section
63892
+ // name (mirrors React's ZoomSlideThumbnail).
63893
+ effect(() => {
63894
+ this.zoomTarget.setSlides(this.mergedSlides());
63895
+ });
63124
63896
  // Connect / disconnect real-time collaboration when the host config changes.
63125
63897
  effect(() => {
63126
63898
  const config = this.collaboration();
@@ -63526,32 +64298,40 @@ class PowerPointViewerComponent {
63526
64298
  this.editor.deleteSelected(this.activeSlideIndex());
63527
64299
  }
63528
64300
  /**
63529
- * Activate the native EyeDropper API to pick a colour from the screen.
64301
+ * Activate the eyedropper to pick a colour from the screen. Uses the native
64302
+ * EyeDropper API where available (Chrome/Edge); on Firefox/Safari it falls
64303
+ * back to a one-shot click that samples the slide DOM under the pointer.
63530
64304
  * When a shape/text/connector/image element is selected, applies the colour
63531
- * to its fill. Otherwise copies the colour to the clipboard. No-ops when
63532
- * the EyeDropper API is not available or the user cancels.
64305
+ * to its fill; otherwise copies it to the clipboard. No-ops when the user
64306
+ * cancels (Escape) or nothing paintable is under the pointer.
63533
64307
  */
63534
64308
  async onToggleEyedropper() {
63535
64309
  this.eyedropperActive.set(true);
63536
64310
  try {
63537
- const color = await openNativeEyeDropper();
64311
+ const color = eyedropperAvailable()
64312
+ ? await openNativeEyeDropper()
64313
+ : await pickColorByClickFallback();
63538
64314
  if (color) {
63539
- const sel = this.selectedElement();
63540
- const idx = this.activeSlideIndex();
63541
- if (sel !== null && hasShapeProperties(sel)) {
63542
- this.editor.updateElement(idx, sel.id, {
63543
- shapeStyle: { ...sel.shapeStyle, fillColor: color },
63544
- });
63545
- }
63546
- else {
63547
- await navigator.clipboard.writeText(color).catch(() => undefined);
63548
- }
64315
+ await this.applyEyedropperColor(color);
63549
64316
  }
63550
64317
  }
63551
64318
  finally {
63552
64319
  this.eyedropperActive.set(false);
63553
64320
  }
63554
64321
  }
64322
+ /** Apply a picked colour to the selected shape's fill, else copy to clipboard. */
64323
+ async applyEyedropperColor(color) {
64324
+ const sel = this.selectedElement();
64325
+ const idx = this.activeSlideIndex();
64326
+ if (sel !== null && hasShapeProperties(sel)) {
64327
+ this.editor.updateElement(idx, sel.id, {
64328
+ shapeStyle: { ...sel.shapeStyle, fillColor: color },
64329
+ });
64330
+ }
64331
+ else {
64332
+ await navigator.clipboard.writeText(color).catch(() => undefined);
64333
+ }
64334
+ }
63555
64335
  /** Append a comment to the active slide (one history entry). */
63556
64336
  onCommentAdd(text) {
63557
64337
  const next = addCommentToList(this.activeComments(), text, 'You');
@@ -64034,6 +64814,7 @@ class PowerPointViewerComponent {
64034
64814
  IsMobileService,
64035
64815
  SmartArt3DService,
64036
64816
  FieldContextService,
64817
+ ZoomTargetService,
64037
64818
  ], viewQueries: [{ propertyName: "mainEl", first: true, predicate: ["mainEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
64038
64819
  <div class="pptx-ng-viewer" [ngClass]="class()" [ngStyle]="rootStyle()">
64039
64820
  @if (loader.loading()) {
@@ -64576,6 +65357,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
64576
65357
  IsMobileService,
64577
65358
  SmartArt3DService,
64578
65359
  FieldContextService,
65360
+ ZoomTargetService,
64579
65361
  ],
64580
65362
  imports: [
64581
65363
  NgClass,