pptx-angular-viewer 1.1.62 → 1.1.64

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.
@@ -21862,7 +21862,7 @@ function connectorBendFraction(element) {
21862
21862
  * for straight connectors (which render as a `<line>`). Endpoints are already
21863
21863
  * flip-adjusted by the caller.
21864
21864
  *
21865
- * Viewer-first approximation (full A* routing is a TODO, see PORTING.md):
21865
+ * Viewer-first approximation (full A* routing is a TODO):
21866
21866
  * - **bent**: orthogonal elbow polyline. `bentConnector2` is a single L-bend;
21867
21867
  * `bentConnector3..5` route through a vertical mid-axis at `bend`.
21868
21868
  * - **curved**: `curvedConnector2` is a quadratic Bezier; `curvedConnector3..5`
@@ -34589,10 +34589,9 @@ function segmentFrameCount(durationMs, fps) {
34589
34589
  * - loader: load-pipeline helpers (media/image collection, guides).
34590
34590
  * - types: CanvasSize, CollaborationConfig, CollaborationRole.
34591
34591
  * - constants: scalar viewer defaults (canvas size, fallback colours).
34592
- *
34593
- * Roadmap (see packages/angular/PORTING.md and packages/vue/PORTING.md):
34594
- * color resolution, geometry/clip-paths, connector routing, animation
34595
- * timeline engine, table-merge math, morph matching, export data helpers.
34592
+ * - render: the bulk of the shared logic (colour/geometry/connector/
34593
+ * animation/table/chart/text/effects/collaboration/i18n).
34594
+ * - export: export data helpers.
34596
34595
  */
34597
34596
 
34598
34597
  /**
@@ -39467,10 +39466,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
39467
39466
  * Provide it at the component level so its lifetime tracks the host viewer:
39468
39467
  * `@Component({ providers: [LoadContentService] })`.
39469
39468
  *
39470
- * This is the viewer-first subset; the React hook also populated ~25 extra
39471
- * pieces of presentation metadata (sections, custom shows, embedded fonts,
39472
- * digital signatures, …). Those are tracked in PORTING.md and added here as
39473
- * the corresponding features are ported.
39469
+ * Originally the viewer-first subset of the React hook; the extra pieces of
39470
+ * presentation metadata (sections, custom shows, embedded fonts, digital
39471
+ * signatures, …) were added alongside the corresponding features.
39474
39472
  */
39475
39473
  class LoadContentService {
39476
39474
  /** Parsed slides (with image Blob URLs patched in). */
@@ -40881,7 +40879,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
40881
40879
  * building the SmartArt element payload from a chosen preset, hierarchy
40882
40880
  * parenting) is delegated to the pure functions below so they can be unit-tested
40883
40881
  * in plain vitest (the Angular package's vitest setup has no Angular compiler,
40884
- * so component / TestBed tests are not available - see PORTING.md).
40882
+ * so component / TestBed tests are not available).
40885
40883
  *
40886
40884
  * The preset catalogue itself is the framework-agnostic, vendored
40887
40885
  * `pptx-viewer-shared` source of truth (`render/smart-art-presets`), consumed
@@ -41495,15 +41493,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
41495
41493
  `, styles: [".pptx-sa-insert{display:flex;gap:0;min-width:min(86vw,560px)}.pptx-sa-insert__sidebar{display:flex;flex-direction:column;width:9rem;flex-shrink:0;border-right:1px solid var(--pptx-border, #e5e7eb);padding:.25rem 0}.pptx-sa-insert__cat{text-align:left;padding:.35rem .75rem;font-size:12px;background:transparent;border:none;color:inherit;cursor:pointer}.pptx-sa-insert__cat:hover{background:var(--pptx-accent, #f1f5f9)}.pptx-sa-insert__cat.is-active{background:var(--pptx-primary, #2563eb);color:#fff}.pptx-sa-insert__main{flex:1;min-width:0;display:flex;flex-direction:column;gap:.5rem;padding:.5rem}.pptx-sa-insert__gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;max-height:16rem;overflow-y:auto}.pptx-sa-insert__cell{display:flex;flex-direction:column;align-items:center;gap:.25rem;padding:.4rem;border:1px solid var(--pptx-border, #e5e7eb);border-radius:4px;background:transparent;color:inherit;cursor:pointer}.pptx-sa-insert__cell:hover{background:var(--pptx-accent, #f1f5f9)}.pptx-sa-insert__cell.is-selected{border-color:var(--pptx-primary, #2563eb);background:color-mix(in srgb,var(--pptx-primary, #2563eb) 18%,transparent)}.pptx-sa-insert__thumb{width:4rem;height:3rem;display:flex;align-items:center;justify-content:center;background:var(--pptx-muted, #f1f5f9);border-radius:4px}.pptx-sa-insert__cell-label{font-size:10px;text-align:center;line-height:1.15}.pptx-sa-insert__text{display:flex;flex-direction:column;gap:.2rem}.pptx-sa-insert__text-label{font-size:10px;color:var(--pptx-muted-foreground, #6b7280)}.pptx-sa-insert__textarea{width:100%;box-sizing:border-box;resize:vertical;font-size:12px;padding:.35rem .5rem;border:1px solid var(--pptx-border, #e5e7eb);border-radius:4px;background:var(--pptx-input, #fff);color:inherit}.pptx-sa-insert__footer{display:flex;gap:.5rem;justify-content:flex-end}.pptx-sa-insert__btn{padding:.35rem .85rem;font-size:12px;border:1px solid var(--pptx-border, #e5e7eb);border-radius:4px;background:var(--pptx-muted, #f1f5f9);color:inherit;cursor:pointer}.pptx-sa-insert__btn--primary{background:var(--pptx-primary, #2563eb);border-color:var(--pptx-primary, #2563eb);color:#fff}.pptx-sa-insert__btn:disabled{opacity:.45;cursor:not-allowed}\n"] }]
41496
41494
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], close: [{ type: i0.Output, args: ["close"] }], insert: [{ type: i0.Output, args: ["insert"] }] } });
41497
41495
 
41498
- /**
41499
- * Thin re-export shim → vendored `pptx-viewer-shared`
41500
- * (`render/inspector-helpers`).
41501
- *
41502
- * The pure inspector-panel value readers + shapeStyle/textStyle patch builders
41503
- * were extracted to shared and are consumed by every binding. This shim
41504
- * preserves the historical Angular import surface.
41505
- */
41506
-
41507
41496
  /**
41508
41497
  * animation-author-helpers.ts: Angular shim over the shared element-animation
41509
41498
  * authoring model.
@@ -46564,6 +46553,15 @@ function stringFromEvent$2(event) {
46564
46553
  return val.length > 0 ? val : null;
46565
46554
  }
46566
46555
 
46556
+ /**
46557
+ * Thin re-export shim → vendored `pptx-viewer-shared`
46558
+ * (`render/inspector-helpers`).
46559
+ *
46560
+ * The pure inspector-panel value readers + shapeStyle/textStyle patch builders
46561
+ * were extracted to shared and are consumed by every binding. This shim
46562
+ * preserves the historical Angular import surface.
46563
+ */
46564
+
46567
46565
  /**
46568
46566
  * smart-art-node-style-helpers.ts: pure logic for per-node SmartArt style
46569
46567
  * editing + node-count boundary constraints in the Angular inspector.
@@ -46680,7 +46678,7 @@ function describeSmartArtBounds(layout) {
46680
46678
  * `editor-insert.ts` (sourced from `pptx-viewer-core`). Keeping this logic out of
46681
46679
  * the component lets it be unit-tested in plain vitest (the Angular package's
46682
46680
  * vitest setup has no Angular compiler, so TestBed component tests are not
46683
- * available - see PORTING.md).
46681
+ * available).
46684
46682
  *
46685
46683
  * Mirrors the React inspector:
46686
46684
  * packages/react/src/viewer/components/inspector/SmartArtPropertiesPanel.tsx
@@ -51390,6 +51388,72 @@ function resolvePresenterNotes(slide) {
51390
51388
  };
51391
51389
  }
51392
51390
 
51391
+ /**
51392
+ * canvas-fit.service.ts: Auto-fit scale measurement for `SlideCanvasComponent`.
51393
+ * Computes the largest scale (<= 1) at which the whole slide fits its scroll
51394
+ * viewport, reserving the 1rem gutter + drop shadow, so the parent's `zoom`
51395
+ * input means "100% of fit" rather than "100% of the authored slide size".
51396
+ * Thumbnail consumers (slides panel, slide sorter) set `autoFit` false and
51397
+ * manage their own scale via `zoom` instead.
51398
+ *
51399
+ * Extracted from {@link SlideCanvasComponent}. Provided per canvas instance
51400
+ * (`providers: [CanvasFitService]` on `SlideCanvasComponent`), so each
51401
+ * canvas (main editor, thumbnails, presentation overlay, ...) measures its
51402
+ * own viewport independently. The component wires the ResizeObserver /
51403
+ * `afterNextRender` lifecycle (both require the component's injection
51404
+ * context) and calls {@link recompute}; this service only owns the
51405
+ * measurement math and the resulting signal.
51406
+ */
51407
+ class CanvasFitService {
51408
+ /**
51409
+ * Auto-fit scale (<= 1): how much the fixed-size slide must shrink to fit
51410
+ * the scroll viewport. The authored slide is e.g. 1280x720, which overflows
51411
+ * a phone; without this it renders off-screen at `zoom=1`.
51412
+ */
51413
+ fitScale = signal(1, /* @ts-ignore */
51414
+ ...(ngDevMode ? [{ debugName: "fitScale" }] : /* istanbul ignore next */ []));
51415
+ host = null;
51416
+ /** Wire the host accessors (called once from the component constructor). */
51417
+ bind(host) {
51418
+ this.host = host;
51419
+ }
51420
+ /**
51421
+ * Recompute {@link fitScale} from the current viewport size. Call after the
51422
+ * view renders and whenever the viewport or slide size may have changed
51423
+ * (ResizeObserver, `canvasSize` change).
51424
+ */
51425
+ recompute() {
51426
+ if (!this.host) {
51427
+ return;
51428
+ }
51429
+ // Thumbnail consumers manage their own scale via `zoom`; keep fit at 1 so
51430
+ // the two scales don't compound.
51431
+ if (!this.host.autoFit()) {
51432
+ this.fitScale.set(1);
51433
+ return;
51434
+ }
51435
+ const el = this.host.viewportElement();
51436
+ const size = this.host.canvasSize();
51437
+ if (!el || !size.width || !size.height) {
51438
+ this.fitScale.set(1);
51439
+ return;
51440
+ }
51441
+ const availW = Math.max(el.clientWidth - 16, 0);
51442
+ const availH = Math.max(el.clientHeight - 32, 0);
51443
+ if (!availW || !availH) {
51444
+ this.fitScale.set(1);
51445
+ return;
51446
+ }
51447
+ const fit = Math.min(availW / size.width, availH / size.height, 1);
51448
+ this.fitScale.set(fit > 0 ? fit : 1);
51449
+ }
51450
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CanvasFitService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
51451
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CanvasFitService });
51452
+ }
51453
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CanvasFitService, decorators: [{
51454
+ type: Injectable
51455
+ }] });
51456
+
51393
51457
  /**
51394
51458
  * Pure geometry for interactive element drag/resize.
51395
51459
  *
@@ -52369,7 +52433,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
52369
52433
  * Compound (double/triple) lines render as parallel strands and line caps map
52370
52434
  * from `a:ln/@cap`; both derive from the shared connector geometry.
52371
52435
  *
52372
- * Not yet ported (TODO, see PORTING.md): line shadows/glow.
52436
+ * Not yet ported (TODO): line shadows/glow.
52373
52437
  */
52374
52438
  class ConnectorRendererComponent {
52375
52439
  element = input.required(/* @ts-ignore */
@@ -52796,7 +52860,8 @@ function buildDuotoneFilter(element) {
52796
52860
  * and paint text boxes, basic preset shapes, images, and image/gradient fills
52797
52861
  * (the latter via the parser's prebuilt CSS gradient string). Advanced visuals
52798
52862
  * (the structured gradient builder, pattern fills, custom geometry clip-paths,
52799
- * shadows, 3D, image effects, text warp) are tracked in PORTING.md.
52863
+ * shadows, 3D, image effects, text warp) are handled by the shared render
52864
+ * modules (`pptx-viewer-shared`) consumed from the renderer components.
52800
52865
  *
52801
52866
  * Long term the *logic* here is a shared-extraction candidate; only the
52802
52867
  * return type (CSS map shape) differs per framework, so a future refactor
@@ -53234,7 +53299,7 @@ function buildInkContainerStyle(element, zIndex) {
53234
53299
  * radii follow the per-point pressure, matching React's `renderInk`. Strokes
53235
53300
  * without pressure variation degrade to a plain constant-width `<path>`.
53236
53301
  *
53237
- * Not ported (TODO, see PORTING.md): ink replay animation and the
53302
+ * Not ported (TODO): ink replay animation and the
53238
53303
  * highlighter/eraser tool blend modes.
53239
53304
  *
53240
53305
  * All non-trivial pure computation lives in `ink-renderer-helpers.ts` (no
@@ -54629,7 +54694,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
54629
54694
  * its text directly on the diagram. Everything that can be expressed as a pure
54630
54695
  * function lives here so it can be unit-tested in plain vitest (the Angular
54631
54696
  * package's vitest setup has no Angular compiler, so component/TestBed tests are
54632
- * not available - see PORTING.md). The component
54697
+ * not available). The component
54633
54698
  * (`smart-art-renderer.component.ts`) stays thin: it owns only the edit-state
54634
54699
  * signal, the positioned `<textarea>`, and the call into the existing commit
54635
54700
  * path (`EditorStateService.updateElement`, the same channel the inspector's
@@ -57294,8 +57359,7 @@ function runStyleFromSegment(seg) {
57294
57359
  * ElementRendererComponent: Angular port of the React `ElementRenderer.tsx`
57295
57360
  * and the Vue `ElementRenderer.vue`.
57296
57361
  *
57297
- * Renders a single slide element by its `type` discriminant (viewer-first
57298
- * subset):
57362
+ * Renders a single slide element by its `type` discriminant:
57299
57363
  * - `text` / `shape` → positioned box with fill/stroke + rich text + effects
57300
57364
  * - `connector` → SVG straight/bent/curved connector
57301
57365
  * - `chart` → inline-SVG chart (bar/line/area/pie/scatter)
@@ -57303,14 +57367,13 @@ function runStyleFromSegment(seg) {
57303
57367
  * - `smartArt` → SVG drawing-shapes / node-text fallback
57304
57368
  * - `ink` → SVG ink strokes
57305
57369
  * - `ole` → embedded-object preview / icon
57306
- * - `model3d` → poster / placeholder (no three.js)
57370
+ * - `model3d` → interactive three.js scene when the optional
57371
+ * `three` peer is present, else poster / placeholder
57307
57372
  * - `zoom` → slide/section zoom thumbnail
57308
57373
  * - `picture` / `image` → `<img>`
57309
57374
  * - `media` → native `<video>`/`<audio>` playback, poster fallback
57310
57375
  * - `group` → recursive children (self-referencing selector)
57311
- * - everything else → labelled placeholder (TODO, see PORTING.md)
57312
- *
57313
- * Interaction (selection, resize, inline editing) is not yet ported.
57376
+ * - everything else → labelled placeholder (defensive fallback)
57314
57377
  */
57315
57378
  class ElementRendererComponent {
57316
57379
  element = input.required(/* @ts-ignore */
@@ -58091,6 +58154,372 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
58091
58154
  }]
58092
58155
  }], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], mediaDataUrls: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaDataUrls", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], obstacles: [{ type: i0.Input, args: [{ isSignal: true, alias: "obstacles", required: false }] }], canvasWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasWidth", required: false }] }], canvasHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasHeight", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], fieldContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "fieldContext", required: false }] }], editTemplateMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editTemplateMode", required: false }] }], cellCommit: [{ type: i0.Output, args: ["cellCommit"] }], tableChange: [{ type: i0.Output, args: ["tableChange"] }] } });
58093
58156
 
58157
+ /**
58158
+ * ink-drawing.service.ts: Pen/highlighter/freeform/eraser drawing state +
58159
+ * logic for `SlideCanvasComponent`'s "draw" branch: capturing a live stroke's
58160
+ * points, the SVG preview path, eraser hit-testing against ink elements, and
58161
+ * finalising a completed stroke into an `InkPptxElement`.
58162
+ *
58163
+ * Extracted from {@link SlideCanvasComponent}. Provided per canvas instance
58164
+ * (`providers: [InkDrawingService]`), so each canvas has its own in-progress
58165
+ * stroke. The component's pointerdown/move/up handlers dispatch to this
58166
+ * service FIRST (draw tools take over all pointer gestures), falling through
58167
+ * to the select/marquee/drag path only when {@link isDrawToolActive} is false
58168
+ * (down) or the move/up delegate methods return `false` (no stroke in
58169
+ * progress).
58170
+ */
58171
+ /** Bounding-box hit-test radius (px, stage coords) for the eraser tool. */
58172
+ const ERASER_HIT_RADIUS = 15;
58173
+ class InkDrawingService {
58174
+ /** Whether a freehand stroke is in progress. Signal for template reactivity. */
58175
+ active = signal(false, /* @ts-ignore */
58176
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
58177
+ /** SVG path `d` for the live stroke preview (updated on every pointer move). */
58178
+ liveInkPath = signal('', /* @ts-ignore */
58179
+ ...(ngDevMode ? [{ debugName: "liveInkPath" }] : /* istanbul ignore next */ []));
58180
+ /** Accumulated points for the stroke currently being drawn. */
58181
+ points = [];
58182
+ host = null;
58183
+ /** Wire the host accessors/emitters (called once from the component constructor). */
58184
+ bind(host) {
58185
+ this.host = host;
58186
+ }
58187
+ requireHost() {
58188
+ if (!this.host) {
58189
+ throw new Error('InkDrawingService.bind() was not called');
58190
+ }
58191
+ return this.host;
58192
+ }
58193
+ /** True when a draw tool (anything but 'select') should own the current gesture. */
58194
+ isDrawToolActive() {
58195
+ return this.requireHost().drawTool() !== 'select';
58196
+ }
58197
+ /**
58198
+ * Handle a stage pointerdown while a draw tool is active: eraser hit-tests
58199
+ * against ink elements (topmost wins); pen/highlighter/freeform begin a new
58200
+ * stroke.
58201
+ */
58202
+ handleStagePointerDown(event) {
58203
+ const host = this.requireHost();
58204
+ const stage = host.stageElement();
58205
+ if (!stage) {
58206
+ return;
58207
+ }
58208
+ const rect = stage.getBoundingClientRect();
58209
+ const zoom = host.effectiveScale() || 1;
58210
+ const pt = {
58211
+ x: (event.clientX - rect.left) / zoom,
58212
+ y: (event.clientY - rect.top) / zoom,
58213
+ };
58214
+ if (host.drawTool() === 'eraser') {
58215
+ // Find ink elements whose bounding box (+ hit radius) contains the
58216
+ // pointer. Iterate in reverse so the topmost element wins.
58217
+ const allElements = host.elements();
58218
+ for (let i = allElements.length - 1; i >= 0; i--) {
58219
+ const el = allElements[i];
58220
+ if (el.type !== 'ink') {
58221
+ continue;
58222
+ }
58223
+ if (pt.x >= el.x - ERASER_HIT_RADIUS &&
58224
+ pt.x <= el.x + el.width + ERASER_HIT_RADIUS &&
58225
+ pt.y >= el.y - ERASER_HIT_RADIUS &&
58226
+ pt.y <= el.y + el.height + ERASER_HIT_RADIUS) {
58227
+ host.emitEraserHit(el.id);
58228
+ break;
58229
+ }
58230
+ }
58231
+ return;
58232
+ }
58233
+ // pen / highlighter / freeform: begin stroke
58234
+ event.preventDefault();
58235
+ event.target?.setPointerCapture?.(event.pointerId);
58236
+ this.points = [pt];
58237
+ this.active.set(true);
58238
+ this.liveInkPath.set(pointsToSvgPathD(this.points));
58239
+ }
58240
+ /** Append a point to the in-progress stroke. Returns false when no stroke is active (caller should fall through). */
58241
+ handlePointerMove(event) {
58242
+ if (!this.active()) {
58243
+ return false;
58244
+ }
58245
+ const host = this.requireHost();
58246
+ const stage = host.stageElement();
58247
+ if (!stage) {
58248
+ return true;
58249
+ }
58250
+ const rect = stage.getBoundingClientRect();
58251
+ const zoom = host.effectiveScale() || 1;
58252
+ const pt = {
58253
+ x: (event.clientX - rect.left) / zoom,
58254
+ y: (event.clientY - rect.top) / zoom,
58255
+ };
58256
+ this.points.push(pt);
58257
+ this.liveInkPath.set(pointsToSvgPathD(this.points));
58258
+ return true;
58259
+ }
58260
+ /** Finalise the in-progress stroke and emit it. Returns false when no stroke was active (caller should fall through). */
58261
+ handlePointerUp() {
58262
+ if (!this.active()) {
58263
+ return false;
58264
+ }
58265
+ const host = this.requireHost();
58266
+ this.active.set(false);
58267
+ const tool = host.drawTool();
58268
+ const resolvedTool = tool === 'highlighter' ? 'highlighter' : tool === 'freeform' ? 'freeform' : 'pen';
58269
+ const ink = strokeToInkElement({
58270
+ points: this.points,
58271
+ color: host.drawColor(),
58272
+ width: host.drawWidth(),
58273
+ tool: resolvedTool,
58274
+ });
58275
+ if (ink) {
58276
+ host.emitInkStrokeComplete(ink);
58277
+ }
58278
+ this.points = [];
58279
+ this.liveInkPath.set('');
58280
+ return true;
58281
+ }
58282
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: InkDrawingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
58283
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: InkDrawingService });
58284
+ }
58285
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: InkDrawingService, decorators: [{
58286
+ type: Injectable
58287
+ }] });
58288
+
58289
+ /**
58290
+ * ruler-guides.service.ts: User-created ruler guide-line state + logic for
58291
+ * `SlideCanvasComponent`: dragging a new guide off a ruler strip, dragging an
58292
+ * existing guide, and removing one (double-click its handle).
58293
+ *
58294
+ * Extracted from {@link SlideCanvasComponent}. Provided per canvas instance
58295
+ * (`providers: [RulerGuidesService]`), so each canvas keeps its own set of
58296
+ * guides. The component's shared pointermove/pointerup handlers check
58297
+ * {@link isDragging} / call {@link handlePointerMove} / {@link handlePointerUp}
58298
+ * FIRST (a guide drag takes over the gesture entirely), falling through to the
58299
+ * draw/marquee/drag path only when no guide drag is in progress. The general
58300
+ * move-drag's guide-snap math stays on the component (it needs the drag's
58301
+ * live `box`), reading {@link rulerGuides} directly.
58302
+ */
58303
+ class RulerGuidesService {
58304
+ /**
58305
+ * User-created guide lines (dragged from rulers or added from toolbar).
58306
+ * axis:'x' -> vertical line at x=pos; axis:'y' -> horizontal line at y=pos.
58307
+ */
58308
+ rulerGuides = signal([], /* @ts-ignore */
58309
+ ...(ngDevMode ? [{ debugName: "rulerGuides" }] : /* istanbul ignore next */ []));
58310
+ /** Active guide-drag state (id + axis only), or null when nothing is being dragged. */
58311
+ guideDrag = null;
58312
+ host = null;
58313
+ /** Wire the host accessors (called once from the component constructor). */
58314
+ bind(host) {
58315
+ this.host = host;
58316
+ }
58317
+ requireHost() {
58318
+ if (!this.host) {
58319
+ throw new Error('RulerGuidesService.bind() was not called');
58320
+ }
58321
+ return this.host;
58322
+ }
58323
+ /** True while an existing or just-created guide is being dragged. */
58324
+ isDragging() {
58325
+ return this.guideDrag !== null;
58326
+ }
58327
+ /** Begin dragging an existing guide. Called from the guide handle pointerdown. */
58328
+ onGuidePointerDown(event, id, axis) {
58329
+ event.stopPropagation();
58330
+ event.target?.setPointerCapture?.(event.pointerId);
58331
+ this.guideDrag = { id, axis };
58332
+ }
58333
+ /** Remove a guide (called on guide handle double-click). */
58334
+ onGuideDoubleClick(event, id) {
58335
+ event.stopPropagation();
58336
+ this.rulerGuides.update((gs) => gs.filter((g) => g.id !== id));
58337
+ }
58338
+ /** Drag from the horizontal ruler to create a new horizontal guide (axis:'y'). */
58339
+ onHRulerPointerDown(event) {
58340
+ const host = this.requireHost();
58341
+ if (!host.editable()) {
58342
+ return;
58343
+ }
58344
+ const stage = host.stageElement();
58345
+ if (!stage) {
58346
+ return;
58347
+ }
58348
+ event.preventDefault();
58349
+ event.target?.setPointerCapture?.(event.pointerId);
58350
+ const rect = stage.getBoundingClientRect();
58351
+ const z = host.effectiveScale() || 1;
58352
+ const pos = (event.clientY - rect.top) / z;
58353
+ const id = `guide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
58354
+ this.rulerGuides.update((gs) => [...gs, { id, axis: 'y', pos: Math.max(0, pos) }]);
58355
+ this.guideDrag = { id, axis: 'y' };
58356
+ }
58357
+ /** Drag from the vertical ruler to create a new vertical guide (axis:'x'). */
58358
+ onVRulerPointerDown(event) {
58359
+ const host = this.requireHost();
58360
+ if (!host.editable()) {
58361
+ return;
58362
+ }
58363
+ const stage = host.stageElement();
58364
+ if (!stage) {
58365
+ return;
58366
+ }
58367
+ event.preventDefault();
58368
+ event.target?.setPointerCapture?.(event.pointerId);
58369
+ const rect = stage.getBoundingClientRect();
58370
+ const z = host.effectiveScale() || 1;
58371
+ const pos = (event.clientX - rect.left) / z;
58372
+ const id = `guide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
58373
+ this.rulerGuides.update((gs) => [...gs, { id, axis: 'x', pos: Math.max(0, pos) }]);
58374
+ this.guideDrag = { id, axis: 'x' };
58375
+ }
58376
+ /** Update the dragged guide's position. Returns false when no guide drag is in progress (caller should fall through). */
58377
+ handlePointerMove(event) {
58378
+ if (!this.guideDrag) {
58379
+ return false;
58380
+ }
58381
+ const host = this.requireHost();
58382
+ const stage = host.stageElement();
58383
+ if (stage) {
58384
+ const rect = stage.getBoundingClientRect();
58385
+ const z = host.effectiveScale() || 1;
58386
+ const guides = this.rulerGuides();
58387
+ const { id, axis } = this.guideDrag;
58388
+ const rawPos = axis === 'x' ? (event.clientX - rect.left) / z : (event.clientY - rect.top) / z;
58389
+ const canvasSize = host.canvasSize();
58390
+ const clampMax = axis === 'x' ? canvasSize.width : canvasSize.height;
58391
+ const pos = Math.max(0, Math.min(clampMax, rawPos));
58392
+ this.rulerGuides.set(guides.map((g) => (g.id === id ? { ...g, pos } : g)));
58393
+ }
58394
+ return true;
58395
+ }
58396
+ /** End the guide drag. Returns false when no guide drag was in progress (caller should fall through). */
58397
+ handlePointerUp() {
58398
+ if (!this.guideDrag) {
58399
+ return false;
58400
+ }
58401
+ this.guideDrag = null;
58402
+ return true;
58403
+ }
58404
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RulerGuidesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
58405
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RulerGuidesService });
58406
+ }
58407
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RulerGuidesService, decorators: [{
58408
+ type: Injectable
58409
+ }] });
58410
+
58411
+ /**
58412
+ * ruler-ticks.ts: Pure tick-mark generation for the slide canvas's horizontal
58413
+ * and vertical ruler strips. No Angular/DOM dependency so it is trivially
58414
+ * unit-testable in isolation from `SlideCanvasComponent`.
58415
+ */
58416
+ /** Height/width (px) of the ruler strips: mirrors React's RULER_THICKNESS. */
58417
+ const RULER_THICKNESS = 20;
58418
+ /** Pixels per inch on the slide canvas (PPTX slides are 10" wide = 960 px). */
58419
+ const SLIDE_PX_PER_INCH = 96;
58420
+ /**
58421
+ * Generate ruler tick marks for a given slide dimension and scale.
58422
+ * Produces ticks every 1/4 inch (minor) and every inch (major).
58423
+ */
58424
+ function generateRulerTicks(slidePx, scale) {
58425
+ const scaledLength = slidePx * scale;
58426
+ const quarterInchPx = (SLIDE_PX_PER_INCH / 4) * scale;
58427
+ if (quarterInchPx < 2) {
58428
+ return [];
58429
+ }
58430
+ const ticks = [];
58431
+ let pos = 0;
58432
+ let inchIndex = 0;
58433
+ while (pos <= scaledLength + 0.5) {
58434
+ const isMajor = inchIndex % 4 === 0;
58435
+ ticks.push({
58436
+ position: pos,
58437
+ isMajor,
58438
+ label: isMajor && inchIndex > 0 ? String(inchIndex / 4) : null,
58439
+ });
58440
+ pos += quarterInchPx;
58441
+ inchIndex++;
58442
+ }
58443
+ return ticks;
58444
+ }
58445
+
58446
+ /**
58447
+ * selection-geometry.ts: Pure geometry helpers for `SlideCanvasComponent`'s
58448
+ * selection outline, resize/rotate/adjust handles, and the
58449
+ * pointer-target-to-interactive-element resolution. No Angular dependency, so
58450
+ * these are trivially unit-testable and reusable outside the component.
58451
+ */
58452
+ /** Bounding boxes (stage coords) for the given selected element ids. */
58453
+ function computeSelectionBoxes(elements, selectedIds) {
58454
+ const selected = new Set(selectedIds);
58455
+ if (selected.size === 0) {
58456
+ return [];
58457
+ }
58458
+ return elements
58459
+ .filter((el) => selected.has(el.id))
58460
+ .map((el) => ({ id: el.id, x: el.x, y: el.y, width: el.width, height: el.height }));
58461
+ }
58462
+ /** The single selected element's box, or null when 0 or >1 are selected. */
58463
+ function computeSingleSelected(elements, selectedIds) {
58464
+ if (selectedIds.length !== 1) {
58465
+ return null;
58466
+ }
58467
+ const el = elements.find((e) => e.id === selectedIds[0]);
58468
+ return el ? { id: el.id, x: el.x, y: el.y, width: el.width, height: el.height } : null;
58469
+ }
58470
+ /** Resize-handle render boxes (stage coords) for the single selection. */
58471
+ function computeHandleBoxes(box, editable, handleScreenPx, zoom) {
58472
+ if (!editable || !box) {
58473
+ return [];
58474
+ }
58475
+ const size = handleScreenPx / (zoom || 1);
58476
+ return RESIZE_HANDLES.map((handle) => {
58477
+ const { fx, fy } = handleAnchor(handle);
58478
+ return {
58479
+ handle,
58480
+ left: box.x + fx * box.width - size / 2,
58481
+ top: box.y + fy * box.height - size / 2,
58482
+ size,
58483
+ cursor: handleCursor(handle),
58484
+ };
58485
+ });
58486
+ }
58487
+ /**
58488
+ * A corner handle box (rotate or shape-adjust) offset above/outside the
58489
+ * single selection's box by `offsetPx` (screen pixels, scaled by zoom).
58490
+ */
58491
+ function computeCornerHandle(box, editable, handleScreenPx, offsetPx, zoom, corner) {
58492
+ if (!editable || !box) {
58493
+ return null;
58494
+ }
58495
+ const z = zoom || 1;
58496
+ const size = handleScreenPx / z;
58497
+ const offset = offsetPx / z;
58498
+ if (corner === 'top-center') {
58499
+ return { left: box.x + box.width / 2 - size / 2, top: box.y - offset - size / 2, size };
58500
+ }
58501
+ return { left: box.x - offset - size / 2, top: box.y - offset - size / 2, size };
58502
+ }
58503
+ /**
58504
+ * Resolve the id of the interactive element under a pointer target, or null.
58505
+ * An element host carries `data-element-id`, but template (master/layout)
58506
+ * elements are only interactive while `editTemplateMode` is on; when off they
58507
+ * are reported as null so the canvas treats them as background (no
58508
+ * select/drag/context-menu/inline-edit).
58509
+ */
58510
+ function resolveInteractiveElementId(target, allElements, editTemplateMode) {
58511
+ const host = target?.closest('[data-element-id]');
58512
+ const id = host?.getAttribute('data-element-id');
58513
+ if (!id) {
58514
+ return null;
58515
+ }
58516
+ const el = allElements.find((e) => e.id === id);
58517
+ if (!el) {
58518
+ return null;
58519
+ }
58520
+ return isElementInteractive(el, true, editTemplateMode) ? id : null;
58521
+ }
58522
+
58094
58523
  /**
58095
58524
  * Thin re-export shim -> vendored `pptx-viewer-shared`.
58096
58525
  *
@@ -58133,36 +58562,6 @@ function isViewportBackgroundPressTarget(target, currentTarget) {
58133
58562
 
58134
58563
  /** Pixels (screen-space) a pointer must move before a click becomes a drag. */
58135
58564
  const DRAG_THRESHOLD = 3;
58136
- // ── Ruler constants ───────────────────────────────────────────────────────────
58137
- /** Height/width (px) of the ruler strips: mirrors React's RULER_THICKNESS. */
58138
- const RULER_THICKNESS = 20;
58139
- /** Pixels per inch on the slide canvas (PPTX slides are 10" wide = 960 px). */
58140
- const SLIDE_PX_PER_INCH = 96;
58141
- /**
58142
- * Generate ruler tick marks for a given slide dimension and scale.
58143
- * Produces ticks every 1/4 inch (minor) and every inch (major).
58144
- */
58145
- function generateRulerTicks(slidePx, scale) {
58146
- const scaledLength = slidePx * scale;
58147
- const quarterInchPx = (SLIDE_PX_PER_INCH / 4) * scale;
58148
- if (quarterInchPx < 2) {
58149
- return [];
58150
- }
58151
- const ticks = [];
58152
- let pos = 0;
58153
- let inchIndex = 0;
58154
- while (pos <= scaledLength + 0.5) {
58155
- const isMajor = inchIndex % 4 === 0;
58156
- ticks.push({
58157
- position: pos,
58158
- isMajor,
58159
- label: isMajor && inchIndex > 0 ? String(inchIndex / 4) : null,
58160
- });
58161
- pos += quarterInchPx;
58162
- inchIndex++;
58163
- }
58164
- return ticks;
58165
- }
58166
58565
  /** Handle size in screen pixels (fine pointer: mouse/trackpad). */
58167
58566
  const HANDLE_SCREEN_PX_FINE = 9;
58168
58567
  /** Handle size in screen pixels (coarse pointer: touch); larger hit target. */
@@ -58196,8 +58595,8 @@ function plainText(el) {
58196
58595
  * Renders the active slide as a fixed-size stage scaled by `zoom`, with each
58197
58596
  * element absolutely positioned. When `editable`, supports click-to-select
58198
58597
  * (event delegation), selection outlines, and pointer drag-to-move / resize
58199
- * handles. Rulers, grid, guides, marquee, and collaboration overlays are
58200
- * tracked in PORTING.md.
58598
+ * handles, plus the rulers, grid, guides, marquee, and collaboration
58599
+ * overlays.
58201
58600
  */
58202
58601
  class SlideCanvasComponent {
58203
58602
  slide = input(undefined, /* @ts-ignore */
@@ -58323,8 +58722,6 @@ class SlideCanvasComponent {
58323
58722
  tableChange = output();
58324
58723
  drag = null;
58325
58724
  editCancelled = false;
58326
- /** Active guide-drag state (id + axis only), or null when nothing is being dragged. */
58327
- guideDrag = null;
58328
58725
  marquee = null;
58329
58726
  /** Live marquee rectangle (stage coords) while rubber-band selecting. */
58330
58727
  marqueeRect = signal(null, /* @ts-ignore */
@@ -58332,42 +58729,24 @@ class SlideCanvasComponent {
58332
58729
  /** Live alignment-snap guide lines (stage coords) during a move. */
58333
58730
  snapGuides = signal([], /* @ts-ignore */
58334
58731
  ...(ngDevMode ? [{ debugName: "snapGuides" }] : /* istanbul ignore next */ []));
58335
- /**
58336
- * User-created guide lines (dragged from rulers or added from toolbar).
58337
- * axis:'x' vertical line at x=pos; axis:'y' → horizontal line at y=pos.
58338
- */
58339
- rulerGuides = signal([], /* @ts-ignore */
58340
- ...(ngDevMode ? [{ debugName: "rulerGuides" }] : /* istanbul ignore next */ []));
58341
- // ── Ink drawing state ─────────────────────────────────────────────────────
58342
- /** Accumulated points for the stroke currently being drawn. */
58343
- inkPoints = [];
58344
- /** Whether a freehand stroke is in progress. Signal for template reactivity. */
58345
- inkActiveSignal = signal(false, /* @ts-ignore */
58346
- ...(ngDevMode ? [{ debugName: "inkActiveSignal" }] : /* istanbul ignore next */ []));
58347
- /** SVG path `d` for the live stroke preview (updated on every pointer move). */
58348
- liveInkPath = signal('', /* @ts-ignore */
58349
- ...(ngDevMode ? [{ debugName: "liveInkPath" }] : /* istanbul ignore next */ []));
58732
+ /** Per-instance pen/highlighter/freeform/eraser drawing controller. */
58733
+ inkDrawing = inject(InkDrawingService);
58734
+ /** Per-instance user-created ruler-guide controller. */
58735
+ rulerGuidesSvc = inject(RulerGuidesService);
58350
58736
  textEditor = viewChild('textEditor', /* @ts-ignore */
58351
58737
  ...(ngDevMode ? [{ debugName: "textEditor" }] : /* istanbul ignore next */ []));
58352
58738
  stageRef = viewChild('stage', /* @ts-ignore */
58353
58739
  ...(ngDevMode ? [{ debugName: "stageRef" }] : /* istanbul ignore next */ []));
58354
58740
  viewportRef = viewChild('viewport', /* @ts-ignore */
58355
58741
  ...(ngDevMode ? [{ debugName: "viewportRef" }] : /* istanbul ignore next */ []));
58356
- /**
58357
- * Auto-fit scale (≤ 1): how much the fixed-size slide must shrink to fit the
58358
- * scroll viewport. The authored slide is e.g. 1280×720, which overflows a
58359
- * phone; without this it renders off-screen at `zoom=1`. Mirrors the React
58360
- * viewer's `fitScale * scale` model (useZoomViewport.ts) so "100%" means "fit
58361
- * to viewport". Measured from the viewport via ResizeObserver below.
58362
- */
58363
- fitScale = signal(1, /* @ts-ignore */
58364
- ...(ngDevMode ? [{ debugName: "fitScale" }] : /* istanbul ignore next */ []));
58742
+ /** Per-instance auto-fit scale measurement (see {@link CanvasFitService}). */
58743
+ canvasFit = inject(CanvasFitService);
58365
58744
  /**
58366
58745
  * The on-screen scale used for ALL rendering and pointer→stage coordinate
58367
58746
  * math: the user's zoom folded with the auto-fit. The parent keeps showing the
58368
58747
  * raw user zoom as the percentage, so this stays internal to the canvas.
58369
58748
  */
58370
- effectiveScale = computed(() => this.fitScale() * this.zoom(), /* @ts-ignore */
58749
+ effectiveScale = computed(() => this.canvasFit.fitScale() * this.zoom(), /* @ts-ignore */
58371
58750
  ...(ngDevMode ? [{ debugName: "effectiveScale" }] : /* istanbul ignore next */ []));
58372
58751
  /** The editing id we've already initialised the textarea for, to avoid re-seeding its value mid-edit. */
58373
58752
  seededEditId = null;
@@ -58400,48 +58779,50 @@ class SlideCanvasComponent {
58400
58779
  const end = editor.nativeElement.value.length;
58401
58780
  editor.nativeElement.setSelectionRange(end, end);
58402
58781
  });
58782
+ // Wire the fit-scale measurement accessors (viewport element, autoFit,
58783
+ // canvasSize all live on this component).
58784
+ this.canvasFit.bind({
58785
+ autoFit: () => this.autoFit(),
58786
+ viewportElement: () => this.viewportRef()?.nativeElement,
58787
+ canvasSize: () => this.canvasSize(),
58788
+ });
58403
58789
  // Re-fit whenever the authored slide size changes (e.g. switching decks).
58404
58790
  effect(() => {
58405
58791
  this.canvasSize();
58406
- this.recomputeFit();
58792
+ this.canvasFit.recompute();
58407
58793
  });
58408
58794
  // Observe the viewport so the slide re-fits on container resize / rotation.
58409
58795
  const destroyRef = inject(DestroyRef);
58410
58796
  afterNextRender(() => {
58411
- this.recomputeFit();
58797
+ this.canvasFit.recompute();
58412
58798
  const el = this.viewportRef()?.nativeElement;
58413
58799
  if (typeof ResizeObserver !== 'undefined' && el) {
58414
- const observer = new ResizeObserver(() => this.recomputeFit());
58800
+ const observer = new ResizeObserver(() => this.canvasFit.recompute());
58415
58801
  observer.observe(el);
58416
58802
  destroyRef.onDestroy(() => observer.disconnect());
58417
58803
  }
58418
58804
  });
58419
- }
58420
- /**
58421
- * Compute the largest scale (≤ 1) at which the whole slide fits the viewport,
58422
- * reserving the 1rem gutter + drop shadow. Sets fitScale to 1 when unmeasured.
58423
- */
58424
- recomputeFit() {
58425
- // Thumbnail consumers manage their own scale via `zoom`; keep fit at 1 so
58426
- // the two scales don't compound.
58427
- if (!this.autoFit()) {
58428
- this.fitScale.set(1);
58429
- return;
58430
- }
58431
- const el = this.viewportRef()?.nativeElement;
58432
- const size = this.canvasSize();
58433
- if (!el || !size.width || !size.height) {
58434
- this.fitScale.set(1);
58435
- return;
58436
- }
58437
- const availW = Math.max(el.clientWidth - 16, 0);
58438
- const availH = Math.max(el.clientHeight - 32, 0);
58439
- if (!availW || !availH) {
58440
- this.fitScale.set(1);
58441
- return;
58442
- }
58443
- const fit = Math.min(availW / size.width, availH / size.height, 1);
58444
- this.fitScale.set(fit > 0 ? fit : 1);
58805
+ // Wire the ink-drawing controller's accessors + emitters (the stage
58806
+ // element, effective scale, and all-elements accessors, plus the two
58807
+ // outputs it completes, all live on this component).
58808
+ this.inkDrawing.bind({
58809
+ stageElement: () => this.stageRef()?.nativeElement,
58810
+ effectiveScale: () => this.effectiveScale(),
58811
+ elements: () => this.elements(),
58812
+ drawTool: () => this.drawTool(),
58813
+ drawColor: () => this.drawColor(),
58814
+ drawWidth: () => this.drawWidth(),
58815
+ emitInkStrokeComplete: (ink) => this.inkStrokeComplete.emit(ink),
58816
+ emitEraserHit: (id) => this.eraserHit.emit(id),
58817
+ });
58818
+ // Wire the ruler-guides controller's accessors (editable, stage element,
58819
+ // effective scale, canvas size all live on this component).
58820
+ this.rulerGuidesSvc.bind({
58821
+ editable: () => this.editable(),
58822
+ stageElement: () => this.stageRef()?.nativeElement,
58823
+ effectiveScale: () => this.effectiveScale(),
58824
+ canvasSize: () => this.canvasSize(),
58825
+ });
58445
58826
  }
58446
58827
  elements = computed(() => this.slide()?.elements ?? [], /* @ts-ignore */
58447
58828
  ...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
@@ -58474,62 +58855,16 @@ class SlideCanvasComponent {
58474
58855
  .map((e) => ({ x: e.x, y: e.y, width: e.width, height: e.height })), /* @ts-ignore */
58475
58856
  ...(ngDevMode ? [{ debugName: "connectorObstacles" }] : /* istanbul ignore next */ []));
58476
58857
  /** Bounding boxes (stage coords) for the selected elements. */
58477
- selectionBoxes = computed(() => {
58478
- const selected = new Set(this.selectedIds());
58479
- if (selected.size === 0) {
58480
- return [];
58481
- }
58482
- return this.allElements()
58483
- .filter((el) => selected.has(el.id))
58484
- .map((el) => ({ id: el.id, x: el.x, y: el.y, width: el.width, height: el.height }));
58485
- }, /* @ts-ignore */
58858
+ selectionBoxes = computed(() => computeSelectionBoxes(this.allElements(), this.selectedIds()), /* @ts-ignore */
58486
58859
  ...(ngDevMode ? [{ debugName: "selectionBoxes" }] : /* istanbul ignore next */ []));
58487
58860
  /** The single selected element's box, or null when 0 or >1 are selected. */
58488
- singleSelected = computed(() => {
58489
- const ids = this.selectedIds();
58490
- if (ids.length !== 1) {
58491
- return null;
58492
- }
58493
- const el = this.allElements().find((e) => e.id === ids[0]);
58494
- return el ? { id: el.id, x: el.x, y: el.y, width: el.width, height: el.height } : null;
58495
- }, /* @ts-ignore */
58861
+ singleSelected = computed(() => computeSingleSelected(this.allElements(), this.selectedIds()), /* @ts-ignore */
58496
58862
  ...(ngDevMode ? [{ debugName: "singleSelected" }] : /* istanbul ignore next */ []));
58497
58863
  /** Resize-handle render boxes (stage coords) for the single selection. */
58498
- handleBoxes = computed(() => {
58499
- if (!this.editable()) {
58500
- return [];
58501
- }
58502
- const box = this.singleSelected();
58503
- if (!box) {
58504
- return [];
58505
- }
58506
- const size = HANDLE_SCREEN_PX / (this.effectiveScale() || 1);
58507
- return RESIZE_HANDLES.map((handle) => {
58508
- const { fx, fy } = handleAnchor(handle);
58509
- return {
58510
- handle,
58511
- left: box.x + fx * box.width - size / 2,
58512
- top: box.y + fy * box.height - size / 2,
58513
- size,
58514
- cursor: handleCursor(handle),
58515
- };
58516
- });
58517
- }, /* @ts-ignore */
58864
+ handleBoxes = computed(() => computeHandleBoxes(this.singleSelected(), this.editable(), HANDLE_SCREEN_PX, this.effectiveScale()), /* @ts-ignore */
58518
58865
  ...(ngDevMode ? [{ debugName: "handleBoxes" }] : /* istanbul ignore next */ []));
58519
58866
  /** Rotation-handle box (stage coords) above the single selection, or null. */
58520
- rotateHandle = computed(() => {
58521
- if (!this.editable()) {
58522
- return null;
58523
- }
58524
- const box = this.singleSelected();
58525
- if (!box) {
58526
- return null;
58527
- }
58528
- const zoom = this.effectiveScale() || 1;
58529
- const size = HANDLE_SCREEN_PX / zoom;
58530
- const offset = 24 / zoom;
58531
- return { left: box.x + box.width / 2 - size / 2, top: box.y - offset - size / 2, size };
58532
- }, /* @ts-ignore */
58867
+ rotateHandle = computed(() => computeCornerHandle(this.singleSelected(), this.editable(), HANDLE_SCREEN_PX, 24, this.effectiveScale(), 'top-center'), /* @ts-ignore */
58533
58868
  ...(ngDevMode ? [{ debugName: "rotateHandle" }] : /* istanbul ignore next */ []));
58534
58869
  /**
58535
58870
  * Shape-adjustment-handle box (stage coords) for the single selection, or
@@ -58537,38 +58872,14 @@ class SlideCanvasComponent {
58537
58872
  * resize/rotate handles. Selection-only + editable-only, so it vanishes in
58538
58873
  * presentation alongside the rest of the edit chrome.
58539
58874
  */
58540
- adjustHandle = computed(() => {
58541
- if (!this.editable()) {
58542
- return null;
58543
- }
58544
- const box = this.singleSelected();
58545
- if (!box) {
58546
- return null;
58547
- }
58548
- const zoom = this.effectiveScale() || 1;
58549
- const size = HANDLE_SCREEN_PX / zoom;
58550
- const offset = 16 / zoom;
58551
- return { left: box.x - offset - size / 2, top: box.y - offset - size / 2, size };
58552
- }, /* @ts-ignore */
58875
+ adjustHandle = computed(() => computeCornerHandle(this.singleSelected(), this.editable(), HANDLE_SCREEN_PX, 16, this.effectiveScale(), 'top-left'), /* @ts-ignore */
58553
58876
  ...(ngDevMode ? [{ debugName: "adjustHandle" }] : /* istanbul ignore next */ []));
58554
58877
  /**
58555
58878
  * Resolve the id of the interactive element under a pointer target, or null.
58556
- * An element host carries `data-element-id`, but template (master/layout)
58557
- * elements are only interactive while editTemplateMode is on; when off they
58558
- * are reported as null so the canvas treats them as background (no
58559
- * select/drag/context-menu/inline-edit).
58879
+ * See {@link resolveInteractiveElementId}.
58560
58880
  */
58561
58881
  interactiveElementIdAt(target) {
58562
- const host = target?.closest('[data-element-id]');
58563
- const id = host?.getAttribute('data-element-id');
58564
- if (!id) {
58565
- return null;
58566
- }
58567
- const el = this.allElements().find((e) => e.id === id);
58568
- if (!el) {
58569
- return null;
58570
- }
58571
- return isElementInteractive(el, true, this.editTemplateMode()) ? id : null;
58882
+ return resolveInteractiveElementId(target, this.allElements(), this.editTemplateMode());
58572
58883
  }
58573
58884
  onStagePointerDown(event) {
58574
58885
  if (!this.editable()) {
@@ -58585,43 +58896,8 @@ class SlideCanvasComponent {
58585
58896
  // ── DRAW BRANCH: must come before the select/marquee/drag path ─────────
58586
58897
  // When a draw tool is active, pointer gestures capture strokes; none of
58587
58898
  // the select/marquee/drag logic should run.
58588
- if (this.drawTool() !== 'select') {
58589
- const stage = this.stageRef()?.nativeElement;
58590
- if (!stage) {
58591
- return;
58592
- }
58593
- const rect = stage.getBoundingClientRect();
58594
- const zoom = this.effectiveScale() || 1;
58595
- const pt = {
58596
- x: (event.clientX - rect.left) / zoom,
58597
- y: (event.clientY - rect.top) / zoom,
58598
- };
58599
- if (this.drawTool() === 'eraser') {
58600
- // Find ink elements whose bounding box (+ 15px hit radius) contains
58601
- // the pointer. Iterate in reverse so the topmost element wins.
58602
- const HIT_RADIUS = 15;
58603
- const allElements = this.elements();
58604
- for (let i = allElements.length - 1; i >= 0; i--) {
58605
- const el = allElements[i];
58606
- if (el.type !== 'ink') {
58607
- continue;
58608
- }
58609
- if (pt.x >= el.x - HIT_RADIUS &&
58610
- pt.x <= el.x + el.width + HIT_RADIUS &&
58611
- pt.y >= el.y - HIT_RADIUS &&
58612
- pt.y <= el.y + el.height + HIT_RADIUS) {
58613
- this.eraserHit.emit(el.id);
58614
- break;
58615
- }
58616
- }
58617
- return;
58618
- }
58619
- // pen / highlighter / freeform: begin stroke
58620
- event.preventDefault();
58621
- event.target?.setPointerCapture?.(event.pointerId);
58622
- this.inkPoints = [pt];
58623
- this.inkActiveSignal.set(true);
58624
- this.liveInkPath.set(pointsToSvgPathD(this.inkPoints));
58899
+ if (this.inkDrawing.isDrawToolActive()) {
58900
+ this.inkDrawing.handleStagePointerDown(event);
58625
58901
  return;
58626
58902
  }
58627
58903
  // ── END DRAW BRANCH ─────────────────────────────────────────────────────
@@ -58831,36 +59107,13 @@ class SlideCanvasComponent {
58831
59107
  }
58832
59108
  onPointerMove(event) {
58833
59109
  // ── GUIDE DRAG ────────────────────────────────────────────────────────
58834
- if (this.guideDrag) {
58835
- const stage = this.stageRef()?.nativeElement;
58836
- if (stage) {
58837
- const rect = stage.getBoundingClientRect();
58838
- const z = this.effectiveScale() || 1;
58839
- const guides = this.rulerGuides();
58840
- const { id, axis } = this.guideDrag;
58841
- const rawPos = axis === 'x' ? (event.clientX - rect.left) / z : (event.clientY - rect.top) / z;
58842
- const clampMax = axis === 'x' ? this.canvasSize().width : this.canvasSize().height;
58843
- const pos = Math.max(0, Math.min(clampMax, rawPos));
58844
- this.rulerGuides.set(guides.map((g) => (g.id === id ? { ...g, pos } : g)));
58845
- }
59110
+ if (this.rulerGuidesSvc.handlePointerMove(event)) {
58846
59111
  return;
58847
59112
  }
58848
59113
  // ── END GUIDE DRAG ────────────────────────────────────────────────────
58849
59114
  // ── DRAW BRANCH ───────────────────────────────────────────────────────
58850
59115
  // When a stroke is in progress, consume all pointer-move events for drawing.
58851
- if (this.inkActiveSignal()) {
58852
- const stage = this.stageRef()?.nativeElement;
58853
- if (!stage) {
58854
- return;
58855
- }
58856
- const rect = stage.getBoundingClientRect();
58857
- const zoom = this.effectiveScale() || 1;
58858
- const pt = {
58859
- x: (event.clientX - rect.left) / zoom,
58860
- y: (event.clientY - rect.top) / zoom,
58861
- };
58862
- this.inkPoints.push(pt);
58863
- this.liveInkPath.set(pointsToSvgPathD(this.inkPoints));
59116
+ if (this.inkDrawing.handlePointerMove(event)) {
58864
59117
  return;
58865
59118
  }
58866
59119
  // ── END DRAW BRANCH ───────────────────────────────────────────────────
@@ -58944,7 +59197,7 @@ class SlideCanvasComponent {
58944
59197
  }
58945
59198
  // Guide snap: snap the moving element to the nearest user guide.
58946
59199
  if (this.snapToGuides()) {
58947
- const guides = this.rulerGuides();
59200
+ const guides = this.rulerGuidesSvc.rulerGuides();
58948
59201
  const thr = SNAP_SCREEN_PX / zoom;
58949
59202
  let gx = box.x;
58950
59203
  let gy = box.y;
@@ -58973,28 +59226,13 @@ class SlideCanvasComponent {
58973
59226
  }
58974
59227
  onPointerUp() {
58975
59228
  // ── GUIDE DRAG ────────────────────────────────────────────────────────
58976
- if (this.guideDrag) {
58977
- this.guideDrag = null;
59229
+ if (this.rulerGuidesSvc.handlePointerUp()) {
58978
59230
  return;
58979
59231
  }
58980
59232
  // ── END GUIDE DRAG ────────────────────────────────────────────────────
58981
59233
  // ── DRAW BRANCH ───────────────────────────────────────────────────────
58982
59234
  // Finalise the stroke and emit the completed ink element.
58983
- if (this.inkActiveSignal()) {
58984
- this.inkActiveSignal.set(false);
58985
- const tool = this.drawTool();
58986
- const resolvedTool = tool === 'highlighter' ? 'highlighter' : tool === 'freeform' ? 'freeform' : 'pen';
58987
- const ink = strokeToInkElement({
58988
- points: this.inkPoints,
58989
- color: this.drawColor(),
58990
- width: this.drawWidth(),
58991
- tool: resolvedTool,
58992
- });
58993
- if (ink) {
58994
- this.inkStrokeComplete.emit(ink);
58995
- }
58996
- this.inkPoints = [];
58997
- this.liveInkPath.set('');
59235
+ if (this.inkDrawing.handlePointerUp()) {
58998
59236
  return;
58999
59237
  }
59000
59238
  // ── END DRAW BRANCH ───────────────────────────────────────────────────
@@ -59017,59 +59255,6 @@ class SlideCanvasComponent {
59017
59255
  this.drag = null;
59018
59256
  this.snapGuides.set([]);
59019
59257
  }
59020
- /** Begin dragging an existing guide. Called from the guide handle pointerdown. */
59021
- onGuidePointerDown(event, id, axis) {
59022
- event.stopPropagation();
59023
- event.target?.setPointerCapture?.(event.pointerId);
59024
- this.guideDrag = { id, axis };
59025
- }
59026
- /** Remove a guide (called on guide handle double-click). */
59027
- onGuideDoubleClick(event, id) {
59028
- event.stopPropagation();
59029
- this.rulerGuides.update((gs) => gs.filter((g) => g.id !== id));
59030
- }
59031
- /** Drag from the horizontal ruler to create a new horizontal guide (axis:'y'). */
59032
- onHRulerPointerDown(event) {
59033
- if (!this.editable()) {
59034
- return;
59035
- }
59036
- const stage = this.stageRef()?.nativeElement;
59037
- if (!stage) {
59038
- return;
59039
- }
59040
- event.preventDefault();
59041
- event.target?.setPointerCapture?.(event.pointerId);
59042
- const rect = stage.getBoundingClientRect();
59043
- const z = this.effectiveScale() || 1;
59044
- const pos = (event.clientY - rect.top) / z;
59045
- const id = `guide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
59046
- this.rulerGuides.update((gs) => [
59047
- ...gs,
59048
- { id, axis: 'y', pos: Math.max(0, pos) },
59049
- ]);
59050
- this.guideDrag = { id, axis: 'y' };
59051
- }
59052
- /** Drag from the vertical ruler to create a new vertical guide (axis:'x'). */
59053
- onVRulerPointerDown(event) {
59054
- if (!this.editable()) {
59055
- return;
59056
- }
59057
- const stage = this.stageRef()?.nativeElement;
59058
- if (!stage) {
59059
- return;
59060
- }
59061
- event.preventDefault();
59062
- event.target?.setPointerCapture?.(event.pointerId);
59063
- const rect = stage.getBoundingClientRect();
59064
- const z = this.effectiveScale() || 1;
59065
- const pos = (event.clientX - rect.left) / z;
59066
- const id = `guide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
59067
- this.rulerGuides.update((gs) => [
59068
- ...gs,
59069
- { id, axis: 'x', pos: Math.max(0, pos) },
59070
- ]);
59071
- this.guideDrag = { id, axis: 'x' };
59072
- }
59073
59258
  wrapperStyle = computed(() => {
59074
59259
  const scale = this.effectiveScale();
59075
59260
  const size = this.canvasSize();
@@ -59139,7 +59324,7 @@ class SlideCanvasComponent {
59139
59324
  }, /* @ts-ignore */
59140
59325
  ...(ngDevMode ? [{ debugName: "stageStyle" }] : /* istanbul ignore next */ []));
59141
59326
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: SlideCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
59142
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: SlideCanvasComponent, isStandalone: true, selector: "pptx-slide-canvas", inputs: { slide: { classPropertyName: "slide", publicName: "slide", isSignal: true, isRequired: false, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, showRulers: { classPropertyName: "showRulers", publicName: "showRulers", isSignal: true, isRequired: false, transformFunction: null }, showGuides: { classPropertyName: "showGuides", publicName: "showGuides", isSignal: true, isRequired: false, transformFunction: null }, snapToGrid: { classPropertyName: "snapToGrid", publicName: "snapToGrid", isSignal: true, isRequired: false, transformFunction: null }, snapToGuides: { classPropertyName: "snapToGuides", publicName: "snapToGuides", isSignal: true, isRequired: false, transformFunction: null }, autoFit: { classPropertyName: "autoFit", publicName: "autoFit", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null }, editingId: { classPropertyName: "editingId", publicName: "editingId", isSignal: true, isRequired: false, transformFunction: null }, editTemplateMode: { classPropertyName: "editTemplateMode", publicName: "editTemplateMode", isSignal: true, isRequired: false, transformFunction: null }, templateElements: { classPropertyName: "templateElements", publicName: "templateElements", isSignal: true, isRequired: false, transformFunction: null }, drawTool: { classPropertyName: "drawTool", publicName: "drawTool", isSignal: true, isRequired: false, transformFunction: null }, drawColor: { classPropertyName: "drawColor", publicName: "drawColor", isSignal: true, isRequired: false, transformFunction: null }, drawWidth: { classPropertyName: "drawWidth", publicName: "drawWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { elementSelect: "elementSelect", backgroundClick: "backgroundClick", transformStart: "transformStart", transformUpdate: "transformUpdate", contextMenu: "contextMenu", textEditStart: "textEditStart", textCommit: "textCommit", textCancel: "textCancel", textFormat: "textFormat", rotateUpdate: "rotateUpdate", marqueeSelect: "marqueeSelect", inkStrokeComplete: "inkStrokeComplete", eraserHit: "eraserHit", cellCommit: "cellCommit", tableChange: "tableChange" }, host: { listeners: { "document:pointermove": "onPointerMove($event)", "document:pointerup": "onPointerUp()" } }, viewQueries: [{ propertyName: "textEditor", first: true, predicate: ["textEditor"], descendants: true, isSignal: true }, { propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }, { propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
59327
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: SlideCanvasComponent, isStandalone: true, selector: "pptx-slide-canvas", inputs: { slide: { classPropertyName: "slide", publicName: "slide", isSignal: true, isRequired: false, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, showRulers: { classPropertyName: "showRulers", publicName: "showRulers", isSignal: true, isRequired: false, transformFunction: null }, showGuides: { classPropertyName: "showGuides", publicName: "showGuides", isSignal: true, isRequired: false, transformFunction: null }, snapToGrid: { classPropertyName: "snapToGrid", publicName: "snapToGrid", isSignal: true, isRequired: false, transformFunction: null }, snapToGuides: { classPropertyName: "snapToGuides", publicName: "snapToGuides", isSignal: true, isRequired: false, transformFunction: null }, autoFit: { classPropertyName: "autoFit", publicName: "autoFit", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null }, editingId: { classPropertyName: "editingId", publicName: "editingId", isSignal: true, isRequired: false, transformFunction: null }, editTemplateMode: { classPropertyName: "editTemplateMode", publicName: "editTemplateMode", isSignal: true, isRequired: false, transformFunction: null }, templateElements: { classPropertyName: "templateElements", publicName: "templateElements", isSignal: true, isRequired: false, transformFunction: null }, drawTool: { classPropertyName: "drawTool", publicName: "drawTool", isSignal: true, isRequired: false, transformFunction: null }, drawColor: { classPropertyName: "drawColor", publicName: "drawColor", isSignal: true, isRequired: false, transformFunction: null }, drawWidth: { classPropertyName: "drawWidth", publicName: "drawWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { elementSelect: "elementSelect", backgroundClick: "backgroundClick", transformStart: "transformStart", transformUpdate: "transformUpdate", contextMenu: "contextMenu", textEditStart: "textEditStart", textCommit: "textCommit", textCancel: "textCancel", textFormat: "textFormat", rotateUpdate: "rotateUpdate", marqueeSelect: "marqueeSelect", inkStrokeComplete: "inkStrokeComplete", eraserHit: "eraserHit", cellCommit: "cellCommit", tableChange: "tableChange" }, host: { listeners: { "document:pointermove": "onPointerMove($event)", "document:pointerup": "onPointerUp()" } }, providers: [CanvasFitService, InkDrawingService, RulerGuidesService], viewQueries: [{ propertyName: "textEditor", first: true, predicate: ["textEditor"], descendants: true, isSignal: true }, { propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }, { propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
59143
59328
  <div
59144
59329
  #viewport
59145
59330
  class="pptx-ng-canvas-viewport"
@@ -59356,7 +59541,7 @@ class SlideCanvasComponent {
59356
59541
  Each guide has a non-interactive line body and an interactive
59357
59542
  drag handle. Double-click the handle to delete the guide.
59358
59543
  -->
59359
- @for (g of rulerGuides(); track g.id) {
59544
+ @for (g of rulerGuidesSvc.rulerGuides(); track g.id) {
59360
59545
  <!-- Guide line body: pointer-events:none -->
59361
59546
  <div
59362
59547
  class="pptx-ng-ruler-guide-line"
@@ -59373,8 +59558,8 @@ class SlideCanvasComponent {
59373
59558
  [style.width]="g.axis === 'x' ? '9px' : '100%'"
59374
59559
  [style.height]="g.axis === 'y' ? '9px' : '100%'"
59375
59560
  [style.cursor]="g.axis === 'x' ? 'col-resize' : 'row-resize'"
59376
- (pointerdown)="onGuidePointerDown($event, g.id, g.axis)"
59377
- (dblclick)="onGuideDoubleClick($event, g.id)"
59561
+ (pointerdown)="rulerGuidesSvc.onGuidePointerDown($event, g.id, g.axis)"
59562
+ (dblclick)="rulerGuidesSvc.onGuideDoubleClick($event, g.id)"
59378
59563
  [title]="'pptx.canvas.guideTooltip' | translate"
59379
59564
  ></div>
59380
59565
  }
@@ -59385,7 +59570,7 @@ class SlideCanvasComponent {
59385
59570
  pointer-events:none so it never intercepts element gestures.
59386
59571
  No data-pptx-element / aria-roledescription / data-pptx-viewport.
59387
59572
  -->
59388
- @if (inkActiveSignal() && liveInkPath() && drawTool() !== 'select') {
59573
+ @if (inkDrawing.active() && inkDrawing.liveInkPath() && drawTool() !== 'select') {
59389
59574
  <svg
59390
59575
  class="pptx-ng-ink-preview"
59391
59576
  aria-hidden="true"
@@ -59394,7 +59579,7 @@ class SlideCanvasComponent {
59394
59579
  style="position:absolute;inset:0;pointer-events:none;z-index:70"
59395
59580
  >
59396
59581
  <path
59397
- [attr.d]="liveInkPath()"
59582
+ [attr.d]="inkDrawing.liveInkPath()"
59398
59583
  fill="none"
59399
59584
  [attr.stroke]="drawColor()"
59400
59585
  [attr.stroke-width]="drawWidth()"
@@ -59424,7 +59609,7 @@ class SlideCanvasComponent {
59424
59609
  [attr.width]="canvasSize().width * effectiveScalePublic()"
59425
59610
  [attr.height]="20"
59426
59611
  [style.cursor]="editable() ? 'crosshair' : null"
59427
- (pointerdown)="editable() ? onHRulerPointerDown($event) : null"
59612
+ (pointerdown)="editable() ? rulerGuidesSvc.onHRulerPointerDown($event) : null"
59428
59613
  >
59429
59614
  <rect
59430
59615
  [attr.width]="canvasSize().width * effectiveScalePublic()"
@@ -59469,7 +59654,7 @@ class SlideCanvasComponent {
59469
59654
  [attr.width]="20"
59470
59655
  [attr.height]="canvasSize().height * effectiveScalePublic()"
59471
59656
  [style.cursor]="editable() ? 'crosshair' : null"
59472
- (pointerdown)="editable() ? onVRulerPointerDown($event) : null"
59657
+ (pointerdown)="editable() ? rulerGuidesSvc.onVRulerPointerDown($event) : null"
59473
59658
  >
59474
59659
  <rect
59475
59660
  width="20"
@@ -59513,7 +59698,7 @@ class SlideCanvasComponent {
59513
59698
  }
59514
59699
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: SlideCanvasComponent, decorators: [{
59515
59700
  type: Component,
59516
- args: [{ selector: 'pptx-slide-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgStyle, ElementRendererComponent, TranslatePipe], template: `
59701
+ args: [{ selector: 'pptx-slide-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: [CanvasFitService, InkDrawingService, RulerGuidesService], imports: [NgStyle, ElementRendererComponent, TranslatePipe], template: `
59517
59702
  <div
59518
59703
  #viewport
59519
59704
  class="pptx-ng-canvas-viewport"
@@ -59730,7 +59915,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59730
59915
  Each guide has a non-interactive line body and an interactive
59731
59916
  drag handle. Double-click the handle to delete the guide.
59732
59917
  -->
59733
- @for (g of rulerGuides(); track g.id) {
59918
+ @for (g of rulerGuidesSvc.rulerGuides(); track g.id) {
59734
59919
  <!-- Guide line body: pointer-events:none -->
59735
59920
  <div
59736
59921
  class="pptx-ng-ruler-guide-line"
@@ -59747,8 +59932,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59747
59932
  [style.width]="g.axis === 'x' ? '9px' : '100%'"
59748
59933
  [style.height]="g.axis === 'y' ? '9px' : '100%'"
59749
59934
  [style.cursor]="g.axis === 'x' ? 'col-resize' : 'row-resize'"
59750
- (pointerdown)="onGuidePointerDown($event, g.id, g.axis)"
59751
- (dblclick)="onGuideDoubleClick($event, g.id)"
59935
+ (pointerdown)="rulerGuidesSvc.onGuidePointerDown($event, g.id, g.axis)"
59936
+ (dblclick)="rulerGuidesSvc.onGuideDoubleClick($event, g.id)"
59752
59937
  [title]="'pptx.canvas.guideTooltip' | translate"
59753
59938
  ></div>
59754
59939
  }
@@ -59759,7 +59944,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59759
59944
  pointer-events:none so it never intercepts element gestures.
59760
59945
  No data-pptx-element / aria-roledescription / data-pptx-viewport.
59761
59946
  -->
59762
- @if (inkActiveSignal() && liveInkPath() && drawTool() !== 'select') {
59947
+ @if (inkDrawing.active() && inkDrawing.liveInkPath() && drawTool() !== 'select') {
59763
59948
  <svg
59764
59949
  class="pptx-ng-ink-preview"
59765
59950
  aria-hidden="true"
@@ -59768,7 +59953,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59768
59953
  style="position:absolute;inset:0;pointer-events:none;z-index:70"
59769
59954
  >
59770
59955
  <path
59771
- [attr.d]="liveInkPath()"
59956
+ [attr.d]="inkDrawing.liveInkPath()"
59772
59957
  fill="none"
59773
59958
  [attr.stroke]="drawColor()"
59774
59959
  [attr.stroke-width]="drawWidth()"
@@ -59798,7 +59983,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59798
59983
  [attr.width]="canvasSize().width * effectiveScalePublic()"
59799
59984
  [attr.height]="20"
59800
59985
  [style.cursor]="editable() ? 'crosshair' : null"
59801
- (pointerdown)="editable() ? onHRulerPointerDown($event) : null"
59986
+ (pointerdown)="editable() ? rulerGuidesSvc.onHRulerPointerDown($event) : null"
59802
59987
  >
59803
59988
  <rect
59804
59989
  [attr.width]="canvasSize().width * effectiveScalePublic()"
@@ -59843,7 +60028,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59843
60028
  [attr.width]="20"
59844
60029
  [attr.height]="canvasSize().height * effectiveScalePublic()"
59845
60030
  [style.cursor]="editable() ? 'crosshair' : null"
59846
- (pointerdown)="editable() ? onVRulerPointerDown($event) : null"
60031
+ (pointerdown)="editable() ? rulerGuidesSvc.onVRulerPointerDown($event) : null"
59847
60032
  >
59848
60033
  <rect
59849
60034
  width="20"
@@ -71431,7 +71616,6 @@ const GRID_GAP = 16;
71431
71616
  * Escape or clicking the ✕ button emits `closed`.
71432
71617
  *
71433
71618
  * Viewer-first scope: no drag-reorder, no context menu, no section grouping.
71434
- * Those features are tracked in PORTING.md.
71435
71619
  *
71436
71620
  * Usage:
71437
71621
  * ```html
@@ -72594,170 +72778,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
72594
72778
  }]
72595
72779
  }], 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"] }] } });
72596
72780
 
72597
- /**
72598
- * viewer-collaboration-session.service.ts: Viewer-scoped state + logic for the
72599
- * Share (two-way collaboration) and Broadcast (one-way owner) sessions. Owns the
72600
- * two dialog visibility flags and the active-session room/server, derives the
72601
- * shareable join / follow links, and drives connect / disconnect on the
72602
- * {@link CollaborationService}.
72603
- *
72604
- * Extracted from {@link PowerPointViewerComponent}: the component binds the host
72605
- * inputs it cannot reach from a service (author name, share defaults, the
72606
- * template-element snapshot supplier) plus the start/stop output emitters via
72607
- * {@link bind}; the template reads the flags / links / defaults and invokes the
72608
- * handlers off the injected instance.
72609
- *
72610
- * Provide it once on the viewer component
72611
- * (`providers: [ViewerCollaborationSessionService]`).
72612
- */
72613
- class ViewerCollaborationSessionService {
72614
- collab = inject(CollaborationService);
72615
- /** Share (collaboration) dialog visibility. */
72616
- showShare = signal(false, /* @ts-ignore */
72617
- ...(ngDevMode ? [{ debugName: "showShare" }] : /* istanbul ignore next */ []));
72618
- /** Broadcast dialog visibility. */
72619
- showBroadcast = signal(false, /* @ts-ignore */
72620
- ...(ngDevMode ? [{ debugName: "showBroadcast" }] : /* istanbul ignore next */ []));
72621
- /**
72622
- * Room/server of the currently active session, used to build the shareable
72623
- * join/follow links shown in the dialogs. Null when no session is active.
72624
- */
72625
- activeSession = signal(null, /* @ts-ignore */
72626
- ...(ngDevMode ? [{ debugName: "activeSession" }] : /* istanbul ignore next */ []));
72627
- host = null;
72628
- /** Wire the host inputs/outputs (called once from the component constructor). */
72629
- bind(host) {
72630
- this.host = host;
72631
- }
72632
- requireHost() {
72633
- if (!this.host) {
72634
- throw new Error('ViewerCollaborationSessionService.bind() was not called');
72635
- }
72636
- return this.host;
72637
- }
72638
- /** Browser location used to assemble share/follow URLs (omitted in SSR). */
72639
- browserLocation() {
72640
- return typeof window === 'undefined'
72641
- ? undefined
72642
- : { origin: window.location.origin, pathname: window.location.pathname };
72643
- }
72644
- /** Shareable join link for the active collaboration session. */
72645
- shareUrl = computed(() => {
72646
- const session = this.activeSession();
72647
- return session ? buildShareUrl(session.roomId, session.serverUrl, this.browserLocation()) : '';
72648
- }, /* @ts-ignore */
72649
- ...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
72650
- /** Shareable follow link for the active broadcast. */
72651
- broadcastViewerUrl = computed(() => {
72652
- const session = this.activeSession();
72653
- return session
72654
- ? buildBroadcastViewerUrl(session.roomId, session.serverUrl, this.browserLocation())
72655
- : '';
72656
- }, /* @ts-ignore */
72657
- ...(ngDevMode ? [{ debugName: "broadcastViewerUrl" }] : /* istanbul ignore next */ []));
72658
- /** Whether the active session is serverless (peer-to-peer / webrtc). */
72659
- activeSessionP2p = computed(() => (this.activeSession()?.serverUrl ?? '').trim().length === 0 && this.collab.active(), /* @ts-ignore */
72660
- ...(ngDevMode ? [{ debugName: "activeSessionP2p" }] : /* istanbul ignore next */ []));
72661
- /**
72662
- * Assemble the {@link ConnectOptions} shared by every connect call site: apply
72663
- * remote slides to the editor, size the presence bounds to the canvas, and
72664
- * expose the source bytes + separated template elements for write-back.
72665
- */
72666
- connectOptions() {
72667
- const host = this.requireHost();
72668
- const size = host.canvasSize();
72669
- return {
72670
- onRemoteSlides: (slides) => host.applyRemoteSlides(slides),
72671
- canvasWidth: size.width,
72672
- canvasHeight: size.height,
72673
- getSourceBytes: () => host.getSourceBytes(),
72674
- getTemplateElements: () => host.getTemplateElements(),
72675
- };
72676
- }
72677
- /**
72678
- * Connect and immediately seed the sync baseline with the current deck, so a
72679
- * joiner whose deck is still a placeholder never broadcasts (and overwrites)
72680
- * the shared document before the first remote sync arrives.
72681
- */
72682
- connectWithBaseline(config) {
72683
- void this.collab.connect(config, this.connectOptions());
72684
- this.collab.seedBaseline(this.requireHost().currentSlides());
72685
- }
72686
- /**
72687
- * Seed values for the Share dialog: the host-supplied `shareDefaults`, with
72688
- * `userName` falling back to `authorName` (then "You") so the local user's
72689
- * name pre-fills the form. Mirrors React/Vue.
72690
- */
72691
- shareDialogDefaults = computed(() => {
72692
- const host = this.host;
72693
- const defaults = host?.shareDefaults() ?? {};
72694
- return {
72695
- ...defaults,
72696
- userName: defaults.userName ?? host?.authorName() ?? 'You',
72697
- };
72698
- }, /* @ts-ignore */
72699
- ...(ngDevMode ? [{ debugName: "shareDialogDefaults" }] : /* istanbul ignore next */ []));
72700
- /**
72701
- * Connect / disconnect real-time collaboration when the host `collaboration`
72702
- * input changes (called from the component's effect).
72703
- */
72704
- syncHostConfig(config) {
72705
- if (config) {
72706
- this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
72707
- this.connectWithBaseline(config);
72708
- }
72709
- else {
72710
- this.collab.disconnect();
72711
- this.activeSession.set(null);
72712
- }
72713
- }
72714
- /** Start a real-time collaboration session from the share dialog config. */
72715
- onShareStart(config) {
72716
- const host = this.requireHost();
72717
- // Two-way collaboration: peers edit together (default `collaborator` role).
72718
- const collaboratorConfig = {
72719
- role: 'collaborator',
72720
- ...config,
72721
- userName: config.userName || (host.authorName() ?? 'You'),
72722
- };
72723
- this.activeSession.set({
72724
- roomId: collaboratorConfig.roomId,
72725
- serverUrl: collaboratorConfig.serverUrl,
72726
- });
72727
- this.connectWithBaseline(collaboratorConfig);
72728
- host.emitStart(collaboratorConfig);
72729
- }
72730
- onShareStop() {
72731
- this.collab.disconnect();
72732
- this.activeSession.set(null);
72733
- this.requireHost().emitStop();
72734
- }
72735
- /** Start broadcasting (presenter as session owner) from the broadcast config. */
72736
- onBroadcastStart(config) {
72737
- const host = this.requireHost();
72738
- const collabConfig = {
72739
- roomId: config.roomId,
72740
- serverUrl: config.serverUrl,
72741
- transport: config.transport,
72742
- userName: host.authorName() ?? 'Presenter',
72743
- role: 'owner',
72744
- };
72745
- this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
72746
- this.connectWithBaseline(collabConfig);
72747
- host.emitStart(collabConfig);
72748
- }
72749
- onBroadcastStop() {
72750
- this.collab.disconnect();
72751
- this.activeSession.set(null);
72752
- this.requireHost().emitStop();
72753
- }
72754
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
72755
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService });
72756
- }
72757
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService, decorators: [{
72758
- type: Injectable
72759
- }] });
72760
-
72761
72781
  /**
72762
72782
  * viewer-dialogs.service.ts: Viewer-scoped open-state + light state for the
72763
72783
  * secondary dialogs and side panels (equation editor, set-up-slide-show,
@@ -72860,6 +72880,694 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
72860
72880
  type: Injectable
72861
72881
  }] });
72862
72882
 
72883
+ /**
72884
+ * EyeDropper colour sampler for the Angular viewer.
72885
+ *
72886
+ * Prefers the native browser EyeDropper API (Chrome 95+ / Edge 95+). For
72887
+ * browsers without it (Firefox, Safari) a DOM-sampling fallback reads the
72888
+ * colour under the pointer via `elementFromPoint` + `getComputedStyle`, so the
72889
+ * eyedropper still works everywhere. Mirrors the React viewer's
72890
+ * `viewer/utils/eyedropper.ts`.
72891
+ */
72892
+ /**
72893
+ * Returns true when the native EyeDropper API is available in this browser.
72894
+ * Guards against SSR / headless environments without `window`.
72895
+ */
72896
+ function eyedropperAvailable() {
72897
+ return typeof window !== 'undefined' && 'EyeDropper' in window;
72898
+ }
72899
+ /**
72900
+ * Open the native EyeDropper and return the picked sRGB hex colour string
72901
+ * (e.g. `"#a3b4c5"`), or `null` when the user cancels or the API is absent.
72902
+ */
72903
+ async function openNativeEyeDropper() {
72904
+ if (!eyedropperAvailable()) {
72905
+ return null;
72906
+ }
72907
+ try {
72908
+ const EyeDropperClass = window['EyeDropper'];
72909
+ const dropper = new EyeDropperClass();
72910
+ const result = await dropper.open();
72911
+ return result.sRGBHex;
72912
+ }
72913
+ catch {
72914
+ // User cancelled (AbortError) or unexpected error: treat as no-op
72915
+ return null;
72916
+ }
72917
+ }
72918
+ // ---------------------------------------------------------------------------
72919
+ // Fallback sampling (Firefox / Safari)
72920
+ // ---------------------------------------------------------------------------
72921
+ function toHex(r, g, b) {
72922
+ const h = (n) => n.toString(16).padStart(2, '0');
72923
+ return `#${h(r)}${h(g)}${h(b)}`;
72924
+ }
72925
+ function parseRgbaString(str) {
72926
+ const match = str.match(/rgba?\(\s*(?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)/u);
72927
+ if (!match?.groups) {
72928
+ return null;
72929
+ }
72930
+ const r = parseInt(match.groups.r, 10);
72931
+ const g = parseInt(match.groups.g, 10);
72932
+ const b = parseInt(match.groups.b, 10);
72933
+ return { r, g, b, hex: toHex(r, g, b) };
72934
+ }
72935
+ /**
72936
+ * Sample the colour at a client-space point by inspecting the topmost element's
72937
+ * computed paint. Tries, in order: a real `<canvas>` pixel read (sharp for
72938
+ * rasterised content), then the element's `background-color`, SVG `fill`, and
72939
+ * finally text `color`. Returns `null` when nothing paintable is found.
72940
+ *
72941
+ * This is the DOM fallback used when the native EyeDropper API is unavailable;
72942
+ * it needs no canvas plumbing, so it works against the live slide DOM.
72943
+ */
72944
+ function sampleColorFromSlide(clientX, clientY) {
72945
+ const target = typeof document === 'undefined' ? null : document.elementFromPoint(clientX, clientY);
72946
+ if (!(target instanceof Element)) {
72947
+ return null;
72948
+ }
72949
+ // Direct pixel read when the pointer is over a <canvas> (untainted only).
72950
+ const canvas = target instanceof HTMLCanvasElement ? target : target.closest('canvas');
72951
+ if (canvas instanceof HTMLCanvasElement) {
72952
+ try {
72953
+ const ctx = canvas.getContext('2d');
72954
+ if (ctx) {
72955
+ const rect = canvas.getBoundingClientRect();
72956
+ const sx = Math.round((clientX - rect.left) * (canvas.width / canvas.clientWidth));
72957
+ const sy = Math.round((clientY - rect.top) * (canvas.height / canvas.clientHeight));
72958
+ const pixel = ctx.getImageData(sx, sy, 1, 1).data;
72959
+ return { r: pixel[0], g: pixel[1], b: pixel[2], hex: toHex(pixel[0], pixel[1], pixel[2]) };
72960
+ }
72961
+ }
72962
+ catch {
72963
+ // Cross-origin / tainted canvas: fall through to computed-style sampling.
72964
+ }
72965
+ }
72966
+ const computed = getComputedStyle(target);
72967
+ const bg = computed.backgroundColor;
72968
+ if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') {
72969
+ const parsed = parseRgbaString(bg);
72970
+ if (parsed) {
72971
+ return parsed;
72972
+ }
72973
+ }
72974
+ const fill = computed.fill;
72975
+ if (fill && fill !== 'none' && fill !== 'transparent') {
72976
+ const parsed = parseRgbaString(fill);
72977
+ if (parsed) {
72978
+ return parsed;
72979
+ }
72980
+ }
72981
+ return computed.color ? parseRgbaString(computed.color) : null;
72982
+ }
72983
+ /**
72984
+ * Run the eyedropper fallback: arm a one-shot pointer listener and resolve with
72985
+ * the hex colour of the next click (or `null` if the user presses Escape). Used
72986
+ * only when {@link eyedropperAvailable} is false. The caller is responsible for
72987
+ * any "armed" UI affordance; this just manages the listeners.
72988
+ */
72989
+ function pickColorByClickFallback() {
72990
+ if (typeof document === 'undefined') {
72991
+ return Promise.resolve(null);
72992
+ }
72993
+ return new Promise((resolve) => {
72994
+ const cleanup = () => {
72995
+ document.removeEventListener('pointerdown', onPointerDown, true);
72996
+ document.removeEventListener('keydown', onKeyDown, true);
72997
+ };
72998
+ const onPointerDown = (event) => {
72999
+ event.preventDefault();
73000
+ event.stopPropagation();
73001
+ cleanup();
73002
+ const sample = sampleColorFromSlide(event.clientX, event.clientY);
73003
+ resolve(sample ? sample.hex : null);
73004
+ };
73005
+ const onKeyDown = (event) => {
73006
+ if (event.key === 'Escape') {
73007
+ cleanup();
73008
+ resolve(null);
73009
+ }
73010
+ };
73011
+ document.addEventListener('pointerdown', onPointerDown, true);
73012
+ document.addEventListener('keydown', onKeyDown, true);
73013
+ });
73014
+ }
73015
+
73016
+ /**
73017
+ * Thin re-export shim → vendored `pptx-viewer-shared`.
73018
+ *
73019
+ * The format-painter copy/apply logic was consolidated into
73020
+ * `pptx-viewer-shared` (`render/format-painter.ts`), shared by every binding.
73021
+ * This shim keeps the historical Angular import path for the component and its
73022
+ * tests.
73023
+ */
73024
+
73025
+ /**
73026
+ * viewer-format-painter.service.ts: Viewer-scoped state + logic for the format
73027
+ * painter (copy one element's shape/text style onto the next clicked element)
73028
+ * and the eyedropper (sample a screen colour onto the selected shape's fill,
73029
+ * else copy it to the clipboard). Both are "apply a style to an element"
73030
+ * interactions keyed off the current selection.
73031
+ *
73032
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the
73033
+ * selection / active-slide accessors via {@link bind}; the template reads the
73034
+ * armed/active flags and invokes the toggles, and the component's selection and
73035
+ * keyboard handlers consult {@link active} / call {@link applyToTarget} /
73036
+ * {@link cancel}.
73037
+ *
73038
+ * Provide it once on the viewer component (`providers: [ViewerFormatPainterService]`).
73039
+ */
73040
+ class ViewerFormatPainterService {
73041
+ editor = inject(EditorStateService);
73042
+ /** True while the painter is armed (next element click applies the copied format). */
73043
+ active = signal(false, /* @ts-ignore */
73044
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
73045
+ /** Whether the eyedropper is currently active. */
73046
+ eyedropperActive = signal(false, /* @ts-ignore */
73047
+ ...(ngDevMode ? [{ debugName: "eyedropperActive" }] : /* istanbul ignore next */ []));
73048
+ /** Format copied from the source element when the painter was armed. */
73049
+ copiedFormat = null;
73050
+ host = null;
73051
+ /** Wire the host selection/slide accessors (called once from the constructor). */
73052
+ bind(host) {
73053
+ this.host = host;
73054
+ }
73055
+ requireHost() {
73056
+ if (!this.host) {
73057
+ throw new Error('ViewerFormatPainterService.bind() was not called');
73058
+ }
73059
+ return this.host;
73060
+ }
73061
+ /** Whether the painter can be armed: exactly one selected element with copyable format. */
73062
+ canActivate = computed(() => hasCopyableFormat(this.host?.selectedElement() ?? null), /* @ts-ignore */
73063
+ ...(ngDevMode ? [{ debugName: "canActivate" }] : /* istanbul ignore next */ []));
73064
+ /** Toggle the format painter: arm from the current selection, or disarm. */
73065
+ toggle() {
73066
+ if (this.active()) {
73067
+ this.cancel();
73068
+ return;
73069
+ }
73070
+ const source = this.requireHost().selectedElement();
73071
+ if (!source || !hasCopyableFormat(source)) {
73072
+ return;
73073
+ }
73074
+ this.copiedFormat = copyFormatFromElement(source);
73075
+ this.active.set(true);
73076
+ }
73077
+ /** Disarm the painter and drop the copied format. */
73078
+ cancel() {
73079
+ this.active.set(false);
73080
+ this.copiedFormat = null;
73081
+ }
73082
+ /** Apply the copied format to a target element (shape/text style only; one history entry). */
73083
+ applyToTarget(id) {
73084
+ const format = this.copiedFormat;
73085
+ const host = this.requireHost();
73086
+ const target = host.findActiveElement(id);
73087
+ if (!format || !target) {
73088
+ return;
73089
+ }
73090
+ const updated = applyFormatToElement(target, format);
73091
+ const patch = {};
73092
+ if (format.shapeStyle && updated['shapeStyle'] !== undefined) {
73093
+ patch['shapeStyle'] = updated['shapeStyle'];
73094
+ }
73095
+ if (format.textStyle && updated['textStyle'] !== undefined) {
73096
+ patch['textStyle'] = updated['textStyle'];
73097
+ }
73098
+ if (Object.keys(patch).length > 0) {
73099
+ this.editor.updateElement(host.activeSlideIndex(), id, patch);
73100
+ }
73101
+ }
73102
+ /**
73103
+ * Activate the eyedropper to pick a colour from the screen. Uses the native
73104
+ * EyeDropper API where available (Chrome/Edge); on Firefox/Safari it falls
73105
+ * back to a one-shot click that samples the slide DOM under the pointer.
73106
+ * When a shape/text/connector/image element is selected, applies the colour
73107
+ * to its fill; otherwise copies it to the clipboard. No-ops when the user
73108
+ * cancels (Escape) or nothing paintable is under the pointer.
73109
+ */
73110
+ async toggleEyedropper() {
73111
+ this.eyedropperActive.set(true);
73112
+ try {
73113
+ const color = eyedropperAvailable()
73114
+ ? await openNativeEyeDropper()
73115
+ : await pickColorByClickFallback();
73116
+ if (color) {
73117
+ await this.applyEyedropperColor(color);
73118
+ }
73119
+ }
73120
+ finally {
73121
+ this.eyedropperActive.set(false);
73122
+ }
73123
+ }
73124
+ /** Apply a picked colour to the selected shape's fill, else copy to clipboard. */
73125
+ async applyEyedropperColor(color) {
73126
+ const host = this.requireHost();
73127
+ const sel = host.selectedElement();
73128
+ const idx = host.activeSlideIndex();
73129
+ if (sel !== null && hasShapeProperties(sel)) {
73130
+ this.editor.updateElement(idx, sel.id, {
73131
+ shapeStyle: { ...sel.shapeStyle, fillColor: color },
73132
+ });
73133
+ }
73134
+ else {
73135
+ await navigator.clipboard.writeText(color).catch(() => undefined);
73136
+ }
73137
+ }
73138
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
73139
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService });
73140
+ }
73141
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService, decorators: [{
73142
+ type: Injectable
73143
+ }] });
73144
+
73145
+ /**
73146
+ * viewer-canvas-editing.service.ts: Viewer-scoped state + logic for direct
73147
+ * canvas interactions on the active slide: element/background/context-menu
73148
+ * selection, inline text edit (incl. the equation-editor detour), ink stroke
73149
+ * completion + eraser hits, table cell/structural edits, and the slide
73150
+ * background/notes property edits. This is the single largest concern pulled
73151
+ * off `PowerPointViewerComponent`'s `SlideCanvasComponent` output wiring.
73152
+ *
73153
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the
73154
+ * few accessors it alone owns (canEdit / active-slide / active-slide-index)
73155
+ * via {@link bind}; the template reads the signals / invokes the handlers off
73156
+ * the injected instance directly (same pattern as `session`/`xport`).
73157
+ *
73158
+ * Provide it once on the viewer component (`providers: [ViewerCanvasEditingService]`).
73159
+ */
73160
+ class ViewerCanvasEditingService {
73161
+ editor = inject(EditorStateService);
73162
+ dialogs = inject(ViewerDialogsService);
73163
+ formatPainter = inject(ViewerFormatPainterService);
73164
+ /** Id of the element being inline text-edited, or null. */
73165
+ editingId = signal(null, /* @ts-ignore */
73166
+ ...(ngDevMode ? [{ debugName: "editingId" }] : /* istanbul ignore next */ []));
73167
+ /** Open editor context-menu position (client coords), or null. */
73168
+ contextMenuPos = signal(null, /* @ts-ignore */
73169
+ ...(ngDevMode ? [{ debugName: "contextMenuPos" }] : /* istanbul ignore next */ []));
73170
+ host = null;
73171
+ /** Wire the host accessors (called once from the component constructor). */
73172
+ bind(host) {
73173
+ this.host = host;
73174
+ }
73175
+ requireHost() {
73176
+ if (!this.host) {
73177
+ throw new Error('ViewerCanvasEditingService.bind() was not called');
73178
+ }
73179
+ return this.host;
73180
+ }
73181
+ /**
73182
+ * Double-click text edit entry: equations open the equation editor instead
73183
+ * of the inline text editor (mirrors React's dbl-click-to-edit-equation).
73184
+ */
73185
+ onTextEditStart(id) {
73186
+ const host = this.requireHost();
73187
+ const element = host.activeSlide()?.elements.find((el) => el.id === id);
73188
+ const segments = element && 'textSegments' in element ? element.textSegments : undefined;
73189
+ const equation = segments?.find((segment) => segment.equationXml);
73190
+ if (host.canEdit() && equation?.equationXml) {
73191
+ this.dialogs.openEquationEdit(id, equation.equationXml);
73192
+ return;
73193
+ }
73194
+ this.editingId.set(id);
73195
+ }
73196
+ /** Apply a Ctrl/Cmd+B/I/U toggle from the inline editor (undoable). */
73197
+ onTextFormat(event) {
73198
+ const host = this.requireHost();
73199
+ if (!host.canEdit()) {
73200
+ return;
73201
+ }
73202
+ const element = host.activeSlide()?.elements.find((el) => el.id === event.id);
73203
+ if (!element) {
73204
+ return;
73205
+ }
73206
+ this.editor.updateElement(host.activeSlideIndex(), event.id, textStylePatch(element, event.updates));
73207
+ }
73208
+ /** Commit an inline text edit: replace the element's text (one history entry). */
73209
+ onTextCommit(event) {
73210
+ const host = this.requireHost();
73211
+ this.editor.updateElement(host.activeSlideIndex(), event.id, {
73212
+ text: event.text,
73213
+ textSegments: [],
73214
+ });
73215
+ this.editingId.set(null);
73216
+ }
73217
+ /** Receive a completed ink stroke and append it to the active slide. */
73218
+ onInkStrokeComplete(ink) {
73219
+ const host = this.requireHost();
73220
+ if (!host.canEdit()) {
73221
+ return;
73222
+ }
73223
+ this.editor.addElement(host.activeSlideIndex(), ink);
73224
+ }
73225
+ /** Receive an eraser hit and delete the targeted ink element. */
73226
+ onEraserHit(id) {
73227
+ const host = this.requireHost();
73228
+ if (!host.canEdit()) {
73229
+ return;
73230
+ }
73231
+ this.editor.select([id]);
73232
+ this.editor.deleteSelected(host.activeSlideIndex());
73233
+ }
73234
+ /**
73235
+ * Handle an element press from the canvas. Additive (Shift/Ctrl) toggles
73236
+ * membership; a plain press selects the element (keeping it selected if it
73237
+ * already was, so a subsequent drag works).
73238
+ */
73239
+ onElementSelect(event) {
73240
+ // The armed format painter intercepts the next element click: apply the
73241
+ // copied format to the target, then disarm (no selection change).
73242
+ if (this.formatPainter.active()) {
73243
+ this.formatPainter.applyToTarget(event.id);
73244
+ this.formatPainter.cancel();
73245
+ return;
73246
+ }
73247
+ if (event.additive) {
73248
+ this.editor.toggleSelect(event.id, true);
73249
+ }
73250
+ else if (!this.editor.isSelected(event.id)) {
73251
+ this.editor.select([event.id]);
73252
+ }
73253
+ }
73254
+ /** Empty-stage press: disarm the painter if armed, else clear the selection. */
73255
+ onBackgroundClick() {
73256
+ if (this.formatPainter.active()) {
73257
+ this.formatPainter.cancel();
73258
+ return;
73259
+ }
73260
+ this.editor.clearSelection();
73261
+ }
73262
+ /** Right-click: select the element under the cursor and open the menu. */
73263
+ onContextMenu(event) {
73264
+ if (event.id && !this.editor.isSelected(event.id)) {
73265
+ this.editor.select([event.id]);
73266
+ }
73267
+ this.contextMenuPos.set({ x: event.x, y: event.y });
73268
+ }
73269
+ /** Update the active slide's background colour. */
73270
+ onSlideBackground(event) {
73271
+ this.editor.updateSlide(this.requireHost().activeSlideIndex(), {
73272
+ backgroundColor: event.target.value,
73273
+ });
73274
+ }
73275
+ /** Update the active slide's speaker notes. */
73276
+ onSlideNotes(event) {
73277
+ this.editor.updateSlide(this.requireHost().activeSlideIndex(), {
73278
+ notes: event.target.value,
73279
+ });
73280
+ }
73281
+ /** Update the active slide's speaker notes from the editable NotesPanel. */
73282
+ onNotesUpdate(notes) {
73283
+ this.editor.updateSlide(this.requireHost().activeSlideIndex(), { notes });
73284
+ }
73285
+ // ── Selection pane handlers ────────────────────────────────────────────────
73286
+ onSelectionPaneBringForward(id) {
73287
+ this.editor.select([id]);
73288
+ this.editor.bringSelectedForward(this.requireHost().activeSlideIndex());
73289
+ }
73290
+ onSelectionPaneSendBackward(id) {
73291
+ this.editor.select([id]);
73292
+ this.editor.sendSelectedBackward(this.requireHost().activeSlideIndex());
73293
+ }
73294
+ onToggleElementHidden(id) {
73295
+ const host = this.requireHost();
73296
+ const el = host.activeSlide()?.elements.find((e) => e.id === id);
73297
+ if (el) {
73298
+ this.editor.updateElement(host.activeSlideIndex(), id, { hidden: !el.hidden });
73299
+ }
73300
+ }
73301
+ /**
73302
+ * Commit a table cell's inline text edit. Finds the table element on the
73303
+ * active slide, rebuilds its `tableData` with the new cell text, and patches
73304
+ * it through the editor (which records undo history).
73305
+ */
73306
+ onTableCellCommit(event) {
73307
+ const host = this.requireHost();
73308
+ if (!host.canEdit()) {
73309
+ return;
73310
+ }
73311
+ const el = host.activeSlide()?.elements.find((e) => e.id === event.id);
73312
+ if (!el || el.type !== 'table') {
73313
+ return;
73314
+ }
73315
+ const updated = setCellText(el, event.commit.rowIndex, event.commit.colIndex, event.commit.text);
73316
+ this.editor.updateElement(host.activeSlideIndex(), event.id, {
73317
+ tableData: updated.tableData,
73318
+ });
73319
+ }
73320
+ /**
73321
+ * Persist a structural table change originating on the canvas (column / row
73322
+ * drag-resize) as one undoable history entry.
73323
+ */
73324
+ onTableChange(event) {
73325
+ const host = this.requireHost();
73326
+ if (!host.canEdit()) {
73327
+ return;
73328
+ }
73329
+ this.editor.updateElement(host.activeSlideIndex(), event.id, {
73330
+ tableData: event.tableData,
73331
+ });
73332
+ }
73333
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCanvasEditingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
73334
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCanvasEditingService });
73335
+ }
73336
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCanvasEditingService, decorators: [{
73337
+ type: Injectable
73338
+ }] });
73339
+
73340
+ /**
73341
+ * viewer-collab-cursor.service.ts: Viewer-scoped logic for the local
73342
+ * collaboration cursor broadcast and the derived remote-cursor overlay list.
73343
+ *
73344
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the
73345
+ * few accessors it alone owns (the `<main>` element, zoom, canvas size, and
73346
+ * active-slide-index) via {@link bind}. `CollaborationService` is already
73347
+ * provided on the component, so it is injected directly rather than passed
73348
+ * through the host.
73349
+ *
73350
+ * Provide it once on the viewer component (`providers: [ViewerCollabCursorService]`).
73351
+ */
73352
+ class ViewerCollabCursorService {
73353
+ collab = inject(CollaborationService);
73354
+ /** Timestamp of the last cursor broadcast (throttle gate). */
73355
+ lastCursorBroadcast = 0;
73356
+ host = null;
73357
+ /** Wire the host accessors (called once from the component constructor). */
73358
+ bind(host) {
73359
+ this.host = host;
73360
+ }
73361
+ requireHost() {
73362
+ if (!this.host) {
73363
+ throw new Error('ViewerCollabCursorService.bind() was not called');
73364
+ }
73365
+ return this.host;
73366
+ }
73367
+ /**
73368
+ * Remote cursors filtered to the slide the local user is viewing, so peers'
73369
+ * cursors only appear on the shared slide (mirrors React/Vue).
73370
+ */
73371
+ cursors = computed(() => presenceToCursors(this.collab.presence(), this.requireHost().activeSlideIndex()), /* @ts-ignore */
73372
+ ...(ngDevMode ? [{ debugName: "cursors" }] : /* istanbul ignore next */ []));
73373
+ /**
73374
+ * Publish the local cursor while the pointer moves over the canvas. Throttled
73375
+ * to {@link BROADCAST_THROTTLE_MS}; coordinates are mapped from client space
73376
+ * into unscaled slide space (dividing by zoom, matching the cursor overlay)
73377
+ * and clamped to the canvas bounds.
73378
+ */
73379
+ onPointerMove(event) {
73380
+ if (!this.collab.active()) {
73381
+ return;
73382
+ }
73383
+ const now = Date.now();
73384
+ if (now - this.lastCursorBroadcast < BROADCAST_THROTTLE_MS) {
73385
+ return;
73386
+ }
73387
+ this.lastCursorBroadcast = now;
73388
+ const host = this.requireHost();
73389
+ const el = host.mainElement();
73390
+ if (!el) {
73391
+ return;
73392
+ }
73393
+ const rect = el.getBoundingClientRect();
73394
+ const zoom = host.zoom() || 1;
73395
+ const size = host.canvasSize();
73396
+ const x = clampCursorPosition((event.clientX - rect.left) / zoom, 0, size.width);
73397
+ const y = clampCursorPosition((event.clientY - rect.top) / zoom, 0, size.height);
73398
+ this.collab.setCursor(x, y, host.activeSlideIndex());
73399
+ }
73400
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollabCursorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
73401
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollabCursorService });
73402
+ }
73403
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollabCursorService, decorators: [{
73404
+ type: Injectable
73405
+ }] });
73406
+
73407
+ /**
73408
+ * viewer-collaboration-session.service.ts: Viewer-scoped state + logic for the
73409
+ * Share (two-way collaboration) and Broadcast (one-way owner) sessions. Owns the
73410
+ * two dialog visibility flags and the active-session room/server, derives the
73411
+ * shareable join / follow links, and drives connect / disconnect on the
73412
+ * {@link CollaborationService}.
73413
+ *
73414
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the host
73415
+ * inputs it cannot reach from a service (author name, share defaults, the
73416
+ * template-element snapshot supplier) plus the start/stop output emitters via
73417
+ * {@link bind}; the template reads the flags / links / defaults and invokes the
73418
+ * handlers off the injected instance.
73419
+ *
73420
+ * Provide it once on the viewer component
73421
+ * (`providers: [ViewerCollaborationSessionService]`).
73422
+ */
73423
+ class ViewerCollaborationSessionService {
73424
+ collab = inject(CollaborationService);
73425
+ /** Share (collaboration) dialog visibility. */
73426
+ showShare = signal(false, /* @ts-ignore */
73427
+ ...(ngDevMode ? [{ debugName: "showShare" }] : /* istanbul ignore next */ []));
73428
+ /** Broadcast dialog visibility. */
73429
+ showBroadcast = signal(false, /* @ts-ignore */
73430
+ ...(ngDevMode ? [{ debugName: "showBroadcast" }] : /* istanbul ignore next */ []));
73431
+ /**
73432
+ * Room/server of the currently active session, used to build the shareable
73433
+ * join/follow links shown in the dialogs. Null when no session is active.
73434
+ */
73435
+ activeSession = signal(null, /* @ts-ignore */
73436
+ ...(ngDevMode ? [{ debugName: "activeSession" }] : /* istanbul ignore next */ []));
73437
+ host = null;
73438
+ /** Wire the host inputs/outputs (called once from the component constructor). */
73439
+ bind(host) {
73440
+ this.host = host;
73441
+ }
73442
+ requireHost() {
73443
+ if (!this.host) {
73444
+ throw new Error('ViewerCollaborationSessionService.bind() was not called');
73445
+ }
73446
+ return this.host;
73447
+ }
73448
+ /** Browser location used to assemble share/follow URLs (omitted in SSR). */
73449
+ browserLocation() {
73450
+ return typeof window === 'undefined'
73451
+ ? undefined
73452
+ : { origin: window.location.origin, pathname: window.location.pathname };
73453
+ }
73454
+ /** Shareable join link for the active collaboration session. */
73455
+ shareUrl = computed(() => {
73456
+ const session = this.activeSession();
73457
+ return session ? buildShareUrl(session.roomId, session.serverUrl, this.browserLocation()) : '';
73458
+ }, /* @ts-ignore */
73459
+ ...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
73460
+ /** Shareable follow link for the active broadcast. */
73461
+ broadcastViewerUrl = computed(() => {
73462
+ const session = this.activeSession();
73463
+ return session
73464
+ ? buildBroadcastViewerUrl(session.roomId, session.serverUrl, this.browserLocation())
73465
+ : '';
73466
+ }, /* @ts-ignore */
73467
+ ...(ngDevMode ? [{ debugName: "broadcastViewerUrl" }] : /* istanbul ignore next */ []));
73468
+ /** Whether the active session is serverless (peer-to-peer / webrtc). */
73469
+ activeSessionP2p = computed(() => (this.activeSession()?.serverUrl ?? '').trim().length === 0 && this.collab.active(), /* @ts-ignore */
73470
+ ...(ngDevMode ? [{ debugName: "activeSessionP2p" }] : /* istanbul ignore next */ []));
73471
+ /**
73472
+ * Assemble the {@link ConnectOptions} shared by every connect call site: apply
73473
+ * remote slides to the editor, size the presence bounds to the canvas, and
73474
+ * expose the source bytes + separated template elements for write-back.
73475
+ */
73476
+ connectOptions() {
73477
+ const host = this.requireHost();
73478
+ const size = host.canvasSize();
73479
+ return {
73480
+ onRemoteSlides: (slides) => host.applyRemoteSlides(slides),
73481
+ canvasWidth: size.width,
73482
+ canvasHeight: size.height,
73483
+ getSourceBytes: () => host.getSourceBytes(),
73484
+ getTemplateElements: () => host.getTemplateElements(),
73485
+ };
73486
+ }
73487
+ /**
73488
+ * Connect and immediately seed the sync baseline with the current deck, so a
73489
+ * joiner whose deck is still a placeholder never broadcasts (and overwrites)
73490
+ * the shared document before the first remote sync arrives.
73491
+ */
73492
+ connectWithBaseline(config) {
73493
+ void this.collab.connect(config, this.connectOptions());
73494
+ this.collab.seedBaseline(this.requireHost().currentSlides());
73495
+ }
73496
+ /**
73497
+ * Seed values for the Share dialog: the host-supplied `shareDefaults`, with
73498
+ * `userName` falling back to `authorName` (then "You") so the local user's
73499
+ * name pre-fills the form. Mirrors React/Vue.
73500
+ */
73501
+ shareDialogDefaults = computed(() => {
73502
+ const host = this.host;
73503
+ const defaults = host?.shareDefaults() ?? {};
73504
+ return {
73505
+ ...defaults,
73506
+ userName: defaults.userName ?? host?.authorName() ?? 'You',
73507
+ };
73508
+ }, /* @ts-ignore */
73509
+ ...(ngDevMode ? [{ debugName: "shareDialogDefaults" }] : /* istanbul ignore next */ []));
73510
+ /**
73511
+ * Connect / disconnect real-time collaboration when the host `collaboration`
73512
+ * input changes (called from the component's effect).
73513
+ */
73514
+ syncHostConfig(config) {
73515
+ if (config) {
73516
+ this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
73517
+ this.connectWithBaseline(config);
73518
+ }
73519
+ else {
73520
+ this.collab.disconnect();
73521
+ this.activeSession.set(null);
73522
+ }
73523
+ }
73524
+ /** Start a real-time collaboration session from the share dialog config. */
73525
+ onShareStart(config) {
73526
+ const host = this.requireHost();
73527
+ // Two-way collaboration: peers edit together (default `collaborator` role).
73528
+ const collaboratorConfig = {
73529
+ role: 'collaborator',
73530
+ ...config,
73531
+ userName: config.userName || (host.authorName() ?? 'You'),
73532
+ };
73533
+ this.activeSession.set({
73534
+ roomId: collaboratorConfig.roomId,
73535
+ serverUrl: collaboratorConfig.serverUrl,
73536
+ });
73537
+ this.connectWithBaseline(collaboratorConfig);
73538
+ host.emitStart(collaboratorConfig);
73539
+ }
73540
+ onShareStop() {
73541
+ this.collab.disconnect();
73542
+ this.activeSession.set(null);
73543
+ this.requireHost().emitStop();
73544
+ }
73545
+ /** Start broadcasting (presenter as session owner) from the broadcast config. */
73546
+ onBroadcastStart(config) {
73547
+ const host = this.requireHost();
73548
+ const collabConfig = {
73549
+ roomId: config.roomId,
73550
+ serverUrl: config.serverUrl,
73551
+ transport: config.transport,
73552
+ userName: host.authorName() ?? 'Presenter',
73553
+ role: 'owner',
73554
+ };
73555
+ this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
73556
+ this.connectWithBaseline(collabConfig);
73557
+ host.emitStart(collabConfig);
73558
+ }
73559
+ onBroadcastStop() {
73560
+ this.collab.disconnect();
73561
+ this.activeSession.set(null);
73562
+ this.requireHost().emitStop();
73563
+ }
73564
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
73565
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService });
73566
+ }
73567
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService, decorators: [{
73568
+ type: Injectable
73569
+ }] });
73570
+
72863
73571
  /**
72864
73572
  * viewer-extra-dialogs-helpers.ts: Pure helpers backing
72865
73573
  * {@link ViewerExtraDialogsComponent}. Kept framework-free (no Angular / DOM)
@@ -73141,6 +73849,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
73141
73849
  type: Injectable
73142
73850
  }] });
73143
73851
 
73852
+ /**
73853
+ * viewer-document-properties.service.ts: Viewer-scoped state + logic for the
73854
+ * Info (document properties) dialog and the hyperlink-edit dialog: their
73855
+ * open/closed signals, the in-session core-properties override merged over
73856
+ * the loaded document properties, and persisting an edit from either dialog.
73857
+ *
73858
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the
73859
+ * few accessors/emitters it alone owns (canEdit / selected-element /
73860
+ * active-slide-index / the `propertiesChange` emitter) via {@link bind}.
73861
+ *
73862
+ * Provide it once on the viewer component (`providers: [ViewerDocumentPropertiesService]`).
73863
+ */
73864
+ class ViewerDocumentPropertiesService {
73865
+ editor = inject(EditorStateService);
73866
+ loader = inject(LoadContentService);
73867
+ /** Document-properties (Info) dialog visibility. */
73868
+ showProperties = signal(false, /* @ts-ignore */
73869
+ ...(ngDevMode ? [{ debugName: "showProperties" }] : /* istanbul ignore next */ []));
73870
+ /** Hyperlink-edit dialog visibility. */
73871
+ showHyperlink = signal(false, /* @ts-ignore */
73872
+ ...(ngDevMode ? [{ debugName: "showHyperlink" }] : /* istanbul ignore next */ []));
73873
+ /** Local overrides applied to document properties via the Info dialog. */
73874
+ coreOverride = signal({}, /* @ts-ignore */
73875
+ ...(ngDevMode ? [{ debugName: "coreOverride" }] : /* istanbul ignore next */ []));
73876
+ /** Document core properties (loaded, with any in-session edits merged in). */
73877
+ coreProperties = computed(() => ({
73878
+ ...(this.loader.coreProperties() ?? {}),
73879
+ ...this.coreOverride(),
73880
+ }), /* @ts-ignore */
73881
+ ...(ngDevMode ? [{ debugName: "coreProperties" }] : /* istanbul ignore next */ []));
73882
+ host = null;
73883
+ /** Wire the host accessors (called once from the component constructor). */
73884
+ bind(host) {
73885
+ this.host = host;
73886
+ }
73887
+ requireHost() {
73888
+ if (!this.host) {
73889
+ throw new Error('ViewerDocumentPropertiesService.bind() was not called');
73890
+ }
73891
+ return this.host;
73892
+ }
73893
+ /**
73894
+ * Persist a document-properties edit from the Info dialog. Gated on
73895
+ * `canEdit`: viewers may inspect properties but not mutate them (mirrors the
73896
+ * comments / hyperlink edit paths).
73897
+ */
73898
+ onPropertiesSave(patch) {
73899
+ const host = this.requireHost();
73900
+ if (!host.canEdit()) {
73901
+ this.showProperties.set(false);
73902
+ return;
73903
+ }
73904
+ this.coreOverride.update((current) => ({ ...current, ...patch }));
73905
+ host.emitPropertiesChange(patch);
73906
+ this.showProperties.set(false);
73907
+ }
73908
+ /** Apply a hyperlink edit to the selected element (one history entry). */
73909
+ onHyperlinkSave(patch) {
73910
+ const host = this.requireHost();
73911
+ const el = host.selectedElement();
73912
+ if (el) {
73913
+ this.editor.updateElement(host.activeSlideIndex(), el.id, patch);
73914
+ }
73915
+ this.showHyperlink.set(false);
73916
+ }
73917
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerDocumentPropertiesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
73918
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerDocumentPropertiesService });
73919
+ }
73920
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerDocumentPropertiesService, decorators: [{
73921
+ type: Injectable
73922
+ }] });
73923
+
73144
73924
  /**
73145
73925
  * viewer-export.service.ts: Viewer-scoped orchestration for the "render every
73146
73926
  * slide to the live stage and capture it" family of actions: single-slide PNG,
@@ -76505,6 +77285,104 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
76505
77285
  }]
76506
77286
  }], ctorParameters: () => [], propDecorators: { activeSlideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSlideIndex", required: false }] }], selectedElementId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedElementId", required: false }] }], filePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "filePath", required: false }] }], customShows: [{ type: i0.Input, args: [{ isSignal: true, alias: "customShows", required: false }] }], restoreContent: [{ type: i0.Output, args: ["restoreContent"] }] } });
76507
77287
 
77288
+ /**
77289
+ * viewer-file-io.service.ts: Viewer-scoped state + logic for getting `.pptx`
77290
+ * bytes in and out of the viewer: the built-in File ▸ Open override signal (a
77291
+ * native-picker-loaded deck swapped in place ahead of the host `content`
77292
+ * input), serialising the current deck to bytes, triggering a browser
77293
+ * download, and resolving the loaded source bytes for collaboration's
77294
+ * elected-writer write-back.
77295
+ *
77296
+ * Extracted from {@link PowerPointViewerComponent}: the component keeps the
77297
+ * public {@link PowerPointViewerComponent.getContent} imperative-handle method
77298
+ * (documented host API, so it cannot move off the component) as a thin
77299
+ * delegate to {@link getContent} here. The component binds the few accessors
77300
+ * it alone owns (canEdit / host `content` input / onOpenFile override / editor
77301
+ * slides + template elements / the `contentChange` emitter) via {@link bind}.
77302
+ *
77303
+ * Provide it once on the viewer component (`providers: [ViewerFileIOService]`).
77304
+ */
77305
+ class ViewerFileIOService {
77306
+ loader = inject(LoadContentService);
77307
+ exportSvc = inject(ExportService);
77308
+ /**
77309
+ * Built-in File ▸ Open override of the host `content` input. The native
77310
+ * picker sets this to swap the deck in place; a fresh `content` input clears
77311
+ * it so external reloads always win.
77312
+ */
77313
+ contentOverride = signal(null, /* @ts-ignore */
77314
+ ...(ngDevMode ? [{ debugName: "contentOverride" }] : /* istanbul ignore next */ []));
77315
+ host = null;
77316
+ /** Wire the host accessors (called once from the component constructor). */
77317
+ bind(host) {
77318
+ this.host = host;
77319
+ }
77320
+ requireHost() {
77321
+ if (!this.host) {
77322
+ throw new Error('ViewerFileIOService.bind() was not called');
77323
+ }
77324
+ return this.host;
77325
+ }
77326
+ /** The currently active content: a picked-file override, else the host `content` input. */
77327
+ activeContent() {
77328
+ return this.contentOverride() ?? this.requireHost().content();
77329
+ }
77330
+ /** The loaded source `.pptx` bytes (for elected-writer write-back), if any. */
77331
+ sourceBytes() {
77332
+ const content = this.activeContent();
77333
+ if (!content) {
77334
+ return null;
77335
+ }
77336
+ return content instanceof Uint8Array ? content : new Uint8Array(content);
77337
+ }
77338
+ /**
77339
+ * Serialise the current presentation to `.pptx` bytes. When editing, this
77340
+ * serialises the editor's edited deck so changes persist.
77341
+ */
77342
+ async getContent() {
77343
+ const host = this.requireHost();
77344
+ const data = host.canEdit()
77345
+ ? await this.loader.saveSlides(buildSaveSlides(host.slides(), host.templateElementsBySlideId()))
77346
+ : await this.loader.getContent();
77347
+ // Mirror React's imperative handle: serialising the deck also notifies the
77348
+ // host so listeners wired to (contentChange) receive the latest bytes.
77349
+ host.emitContentChange(data);
77350
+ return data;
77351
+ }
77352
+ /**
77353
+ * Serialise the current deck and trigger a browser download of the `.pptx`.
77354
+ * Surfaced on the mobile toolbar so saving is reachable without the desktop
77355
+ * ribbon's File tab.
77356
+ */
77357
+ async saveAsPptx() {
77358
+ const bytes = await this.getContent();
77359
+ this.exportSvc.savePptx(bytes, 'presentation.pptx');
77360
+ }
77361
+ /**
77362
+ * File ▸ Open: host override (`onOpenFile` input) takes precedence; otherwise
77363
+ * a built-in native picker loads the chosen presentation in place.
77364
+ */
77365
+ openFile() {
77366
+ const host = this.requireHost();
77367
+ const override = host.onOpenFile();
77368
+ if (override) {
77369
+ override();
77370
+ return;
77371
+ }
77372
+ void (async () => {
77373
+ const picked = await openPptxFile();
77374
+ if (picked) {
77375
+ this.contentOverride.set(new Uint8Array(picked.buffer));
77376
+ }
77377
+ })();
77378
+ }
77379
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFileIOService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77380
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFileIOService });
77381
+ }
77382
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFileIOService, decorators: [{
77383
+ type: Injectable
77384
+ }] });
77385
+
76508
77386
  /**
76509
77387
  * viewer-find-replace.service.ts: Viewer-scoped state + logic for the
76510
77388
  * find-in-slides bar and the edit-mode find-and-replace bar. Owns the two bar
@@ -76596,264 +77474,177 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
76596
77474
  }] });
76597
77475
 
76598
77476
  /**
76599
- * EyeDropper colour sampler for the Angular viewer.
77477
+ * swipe-dismiss.ts: Reusable pointer-drag-to-dismiss gesture for a docked
77478
+ * sheet/panel that lives in normal document flow (not a fixed overlay), so a
77479
+ * downward swipe is surfaced as a live drag offset (applied via a CSS
77480
+ * transform) rather than relying on a native overlay's dismiss gesture.
76600
77481
  *
76601
- * Prefers the native browser EyeDropper API (Chrome 95+ / Edge 95+). For
76602
- * browsers without it (Firefox, Safari) a DOM-sampling fallback reads the
76603
- * colour under the pointer via `elementFromPoint` + `getComputedStyle`, so the
76604
- * eyedropper still works everywhere. Mirrors the React viewer's
76605
- * `viewer/utils/eyedropper.ts`.
76606
- */
76607
- /**
76608
- * Returns true when the native EyeDropper API is available in this browser.
76609
- * Guards against SSR / headless environments without `window`.
76610
- */
76611
- function eyedropperAvailable() {
76612
- return typeof window !== 'undefined' && 'EyeDropper' in window;
76613
- }
76614
- /**
76615
- * Open the native EyeDropper and return the picked sRGB hex colour string
76616
- * (e.g. `"#a3b4c5"`), or `null` when the user cancels or the API is absent.
77482
+ * Shared by the mobile notes sheet and the mobile inspector host in
77483
+ * {@link PowerPointViewerComponent}, which both need identical drag-to-dismiss
77484
+ * behaviour, differing only in what happens once the drag clears the
77485
+ * threshold (their own `onDismiss` callback).
76617
77486
  */
76618
- async function openNativeEyeDropper() {
76619
- if (!eyedropperAvailable()) {
76620
- return null;
76621
- }
76622
- try {
76623
- const EyeDropperClass = window['EyeDropper'];
76624
- const dropper = new EyeDropperClass();
76625
- const result = await dropper.open();
76626
- return result.sRGBHex;
76627
- }
76628
- catch {
76629
- // User cancelled (AbortError) or unexpected error: treat as no-op
76630
- return null;
76631
- }
76632
- }
76633
- // ---------------------------------------------------------------------------
76634
- // Fallback sampling (Firefox / Safari)
76635
- // ---------------------------------------------------------------------------
76636
- function toHex(r, g, b) {
76637
- const h = (n) => n.toString(16).padStart(2, '0');
76638
- return `#${h(r)}${h(g)}${h(b)}`;
76639
- }
76640
- function parseRgbaString(str) {
76641
- const match = str.match(/rgba?\(\s*(?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)/u);
76642
- if (!match?.groups) {
76643
- return null;
76644
- }
76645
- const r = parseInt(match.groups.r, 10);
76646
- const g = parseInt(match.groups.g, 10);
76647
- const b = parseInt(match.groups.b, 10);
76648
- return { r, g, b, hex: toHex(r, g, b) };
76649
- }
77487
+ /** Downward drag distance (px) past which releasing the pointer dismisses. Matches `pptx-mobile-sheet`'s DISMISS_THRESHOLD. */
77488
+ const DISMISS_THRESHOLD_PX = 120;
76650
77489
  /**
76651
- * Sample the colour at a client-space point by inspecting the topmost element's
76652
- * computed paint. Tries, in order: a real `<canvas>` pixel read (sharp for
76653
- * rasterised content), then the element's `background-color`, SVG `fill`, and
76654
- * finally text `color`. Returns `null` when nothing paintable is found.
76655
- *
76656
- * This is the DOM fallback used when the native EyeDropper API is unavailable;
76657
- * it needs no canvas plumbing, so it works against the live slide DOM.
77490
+ * Create an independent swipe-to-dismiss drag tracker. `onDismiss` fires once,
77491
+ * on pointer-up, when the downward drag exceeded {@link DISMISS_THRESHOLD_PX}.
76658
77492
  */
76659
- function sampleColorFromSlide(clientX, clientY) {
76660
- const target = typeof document === 'undefined' ? null : document.elementFromPoint(clientX, clientY);
76661
- if (!(target instanceof Element)) {
76662
- return null;
76663
- }
76664
- // Direct pixel read when the pointer is over a <canvas> (untainted only).
76665
- const canvas = target instanceof HTMLCanvasElement ? target : target.closest('canvas');
76666
- if (canvas instanceof HTMLCanvasElement) {
76667
- try {
76668
- const ctx = canvas.getContext('2d');
76669
- if (ctx) {
76670
- const rect = canvas.getBoundingClientRect();
76671
- const sx = Math.round((clientX - rect.left) * (canvas.width / canvas.clientWidth));
76672
- const sy = Math.round((clientY - rect.top) * (canvas.height / canvas.clientHeight));
76673
- const pixel = ctx.getImageData(sx, sy, 1, 1).data;
76674
- return { r: pixel[0], g: pixel[1], b: pixel[2], hex: toHex(pixel[0], pixel[1], pixel[2]) };
77493
+ function createSwipeDismissDrag(onDismiss) {
77494
+ const dragYSignal = signal(0, /* @ts-ignore */
77495
+ ...(ngDevMode ? [{ debugName: "dragYSignal" }] : /* istanbul ignore next */ []));
77496
+ const draggingSignal = signal(false, /* @ts-ignore */
77497
+ ...(ngDevMode ? [{ debugName: "draggingSignal" }] : /* istanbul ignore next */ []));
77498
+ let startY = null;
77499
+ return {
77500
+ dragY: dragYSignal,
77501
+ dragging: draggingSignal,
77502
+ onPointerDown(event) {
77503
+ startY = event.clientY;
77504
+ draggingSignal.set(true);
77505
+ event.target.setPointerCapture?.(event.pointerId);
77506
+ },
77507
+ onPointerMove(event) {
77508
+ if (startY === null) {
77509
+ return;
76675
77510
  }
76676
- }
76677
- catch {
76678
- // Cross-origin / tainted canvas: fall through to computed-style sampling.
76679
- }
76680
- }
76681
- const computed = getComputedStyle(target);
76682
- const bg = computed.backgroundColor;
76683
- if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') {
76684
- const parsed = parseRgbaString(bg);
76685
- if (parsed) {
76686
- return parsed;
76687
- }
76688
- }
76689
- const fill = computed.fill;
76690
- if (fill && fill !== 'none' && fill !== 'transparent') {
76691
- const parsed = parseRgbaString(fill);
76692
- if (parsed) {
76693
- return parsed;
76694
- }
76695
- }
76696
- return computed.color ? parseRgbaString(computed.color) : null;
76697
- }
76698
- /**
76699
- * Run the eyedropper fallback: arm a one-shot pointer listener and resolve with
76700
- * the hex colour of the next click (or `null` if the user presses Escape). Used
76701
- * only when {@link eyedropperAvailable} is false. The caller is responsible for
76702
- * any "armed" UI affordance; this just manages the listeners.
76703
- */
76704
- function pickColorByClickFallback() {
76705
- if (typeof document === 'undefined') {
76706
- return Promise.resolve(null);
76707
- }
76708
- return new Promise((resolve) => {
76709
- const cleanup = () => {
76710
- document.removeEventListener('pointerdown', onPointerDown, true);
76711
- document.removeEventListener('keydown', onKeyDown, true);
76712
- };
76713
- const onPointerDown = (event) => {
76714
- event.preventDefault();
76715
- event.stopPropagation();
76716
- cleanup();
76717
- const sample = sampleColorFromSlide(event.clientX, event.clientY);
76718
- resolve(sample ? sample.hex : null);
76719
- };
76720
- const onKeyDown = (event) => {
76721
- if (event.key === 'Escape') {
76722
- cleanup();
76723
- resolve(null);
77511
+ dragYSignal.set(Math.max(0, event.clientY - startY));
77512
+ },
77513
+ onPointerUp(event) {
77514
+ if (startY === null) {
77515
+ return;
76724
77516
  }
76725
- };
76726
- document.addEventListener('pointerdown', onPointerDown, true);
76727
- document.addEventListener('keydown', onKeyDown, true);
76728
- });
77517
+ const delta = event.clientY - startY;
77518
+ startY = null;
77519
+ draggingSignal.set(false);
77520
+ event.target.releasePointerCapture?.(event.pointerId);
77521
+ if (delta > DISMISS_THRESHOLD_PX) {
77522
+ onDismiss();
77523
+ }
77524
+ dragYSignal.set(0);
77525
+ },
77526
+ };
76729
77527
  }
76730
77528
 
76731
77529
  /**
76732
- * Thin re-export shim vendored `pptx-viewer-shared`.
76733
- *
76734
- * The format-painter copy/apply logic was consolidated into
76735
- * `pptx-viewer-shared` (`render/format-painter.ts`), shared by every binding.
76736
- * This shim keeps the historical Angular import path for the component and its
76737
- * tests.
76738
- */
76739
-
76740
- /**
76741
- * viewer-format-painter.service.ts: Viewer-scoped state + logic for the format
76742
- * painter (copy one element's shape/text style onto the next clicked element)
76743
- * and the eyedropper (sample a screen colour onto the selected shape's fill,
76744
- * else copy it to the clipboard). Both are "apply a style to an element"
76745
- * interactions keyed off the current selection.
77530
+ * viewer-inspector-panel.service.ts: Viewer-scoped state + logic for the
77531
+ * single right-docked inspector host: which explicit tool panel (comments /
77532
+ * accessibility / signatures / selection) is toggled on, the derived "what
77533
+ * should the host actually show" precedence (explicit panel element
77534
+ * slide default), its mobile-only swipe-to-dismiss drag, and the accessible
77535
+ * label per shown content.
76746
77536
  *
76747
77537
  * Extracted from {@link PowerPointViewerComponent}: the component binds the
76748
- * selection / active-slide accessors via {@link bind}; the template reads the
76749
- * armed/active flags and invokes the toggles, and the component's selection and
76750
- * keyboard handlers consult {@link active} / call {@link applyToTarget} /
76751
- * {@link cancel}.
77538
+ * accessors it alone owns (canEdit / selected-element / active-slide) via
77539
+ * {@link bind}; the template reads the signals/computeds off the injected
77540
+ * instance directly (same pattern as `session`/`xport`).
76752
77541
  *
76753
- * Provide it once on the viewer component (`providers: [ViewerFormatPainterService]`).
77542
+ * Provide it once on the viewer component (`providers: [ViewerInspectorPanelService]`).
76754
77543
  */
76755
- class ViewerFormatPainterService {
76756
- editor = inject(EditorStateService);
76757
- /** True while the painter is armed (next element click applies the copied format). */
76758
- active = signal(false, /* @ts-ignore */
76759
- ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
76760
- /** Whether the eyedropper is currently active. */
76761
- eyedropperActive = signal(false, /* @ts-ignore */
76762
- ...(ngDevMode ? [{ debugName: "eyedropperActive" }] : /* istanbul ignore next */ []));
76763
- /** Format copied from the source element when the painter was armed. */
76764
- copiedFormat = null;
77544
+ class ViewerInspectorPanelService {
77545
+ mobile = inject(IsMobileService);
77546
+ /** Active right-docked tool panel (comments / accessibility / selection), or null. */
77547
+ activePanel = signal(null, /* @ts-ignore */
77548
+ ...(ngDevMode ? [{ debugName: "activePanel" }] : /* istanbul ignore next */ []));
77549
+ /** True once the user swiped the inspector away on mobile (until reopened). */
77550
+ mobileInspectorHidden = signal(false, /* @ts-ignore */
77551
+ ...(ngDevMode ? [{ debugName: "mobileInspectorHidden" }] : /* istanbul ignore next */ []));
77552
+ /**
77553
+ * Swipe-to-dismiss drag for the inspector host. The host docks in-flow below
77554
+ * the canvas on mobile (same keyboard-reachability reason as the notes
77555
+ * sheet), so the gesture is wired here rather than via a fixed-overlay
77556
+ * dismiss. Clearing the past-threshold drag also closes any open tool panel.
77557
+ */
77558
+ inspectorDrag = createSwipeDismissDrag(() => {
77559
+ this.mobileInspectorHidden.set(true);
77560
+ this.activePanel.set(null);
77561
+ });
76765
77562
  host = null;
76766
- /** Wire the host selection/slide accessors (called once from the constructor). */
77563
+ /** Wire the host accessors (called once from the component constructor). */
76767
77564
  bind(host) {
76768
77565
  this.host = host;
76769
77566
  }
76770
77567
  requireHost() {
76771
77568
  if (!this.host) {
76772
- throw new Error('ViewerFormatPainterService.bind() was not called');
77569
+ throw new Error('ViewerInspectorPanelService.bind() was not called');
76773
77570
  }
76774
77571
  return this.host;
76775
77572
  }
76776
- /** Whether the painter can be armed: exactly one selected element with copyable format. */
76777
- canActivate = computed(() => hasCopyableFormat(this.host?.selectedElement() ?? null), /* @ts-ignore */
76778
- ...(ngDevMode ? [{ debugName: "canActivate" }] : /* istanbul ignore next */ []));
76779
- /** Toggle the format painter: arm from the current selection, or disarm. */
76780
- toggle() {
76781
- if (this.active()) {
76782
- this.cancel();
76783
- return;
76784
- }
76785
- const source = this.requireHost().selectedElement();
76786
- if (!source || !hasCopyableFormat(source)) {
76787
- return;
76788
- }
76789
- this.copiedFormat = copyFormatFromElement(source);
76790
- this.active.set(true);
76791
- }
76792
- /** Disarm the painter and drop the copied format. */
76793
- cancel() {
76794
- this.active.set(false);
76795
- this.copiedFormat = null;
76796
- }
76797
- /** Apply the copied format to a target element (shape/text style only; one history entry). */
76798
- applyToTarget(id) {
76799
- const format = this.copiedFormat;
77573
+ /**
77574
+ * Which panel the single inspector host should show, applying the original
77575
+ * first-match precedence (explicit tool panels element slide default).
77576
+ * `accessibility`/`signatures` render regardless of edit mode; the rest need
77577
+ * `canEdit`.
77578
+ */
77579
+ inspectorContent = computed(() => {
76800
77580
  const host = this.requireHost();
76801
- const target = host.findActiveElement(id);
76802
- if (!format || !target) {
76803
- return;
77581
+ const panel = this.activePanel();
77582
+ if (panel === 'accessibility') {
77583
+ return 'accessibility';
76804
77584
  }
76805
- const updated = applyFormatToElement(target, format);
76806
- const patch = {};
76807
- if (format.shapeStyle && updated['shapeStyle'] !== undefined) {
76808
- patch['shapeStyle'] = updated['shapeStyle'];
77585
+ if (panel === 'signatures') {
77586
+ return 'signatures';
76809
77587
  }
76810
- if (format.textStyle && updated['textStyle'] !== undefined) {
76811
- patch['textStyle'] = updated['textStyle'];
77588
+ if (!host.canEdit()) {
77589
+ return null;
76812
77590
  }
76813
- if (Object.keys(patch).length > 0) {
76814
- this.editor.updateElement(host.activeSlideIndex(), id, patch);
77591
+ if (panel === 'comments') {
77592
+ return 'comments';
76815
77593
  }
76816
- }
76817
- /**
76818
- * Activate the eyedropper to pick a colour from the screen. Uses the native
76819
- * EyeDropper API where available (Chrome/Edge); on Firefox/Safari it falls
76820
- * back to a one-shot click that samples the slide DOM under the pointer.
76821
- * When a shape/text/connector/image element is selected, applies the colour
76822
- * to its fill; otherwise copies it to the clipboard. No-ops when the user
76823
- * cancels (Escape) or nothing paintable is under the pointer.
76824
- */
76825
- async toggleEyedropper() {
76826
- this.eyedropperActive.set(true);
76827
- try {
76828
- const color = eyedropperAvailable()
76829
- ? await openNativeEyeDropper()
76830
- : await pickColorByClickFallback();
76831
- if (color) {
76832
- await this.applyEyedropperColor(color);
76833
- }
77594
+ if (panel === 'selection') {
77595
+ return 'selection';
76834
77596
  }
76835
- finally {
76836
- this.eyedropperActive.set(false);
77597
+ if (host.selectedElement()) {
77598
+ return 'element';
76837
77599
  }
76838
- }
76839
- /** Apply a picked colour to the selected shape's fill, else copy to clipboard. */
76840
- async applyEyedropperColor(color) {
76841
- const host = this.requireHost();
76842
- const sel = host.selectedElement();
76843
- const idx = host.activeSlideIndex();
76844
- if (sel !== null && hasShapeProperties(sel)) {
76845
- this.editor.updateElement(idx, sel.id, {
76846
- shapeStyle: { ...sel.shapeStyle, fillColor: color },
76847
- });
77600
+ if (host.activeSlide()) {
77601
+ return 'slide';
76848
77602
  }
76849
- else {
76850
- await navigator.clipboard.writeText(color).catch(() => undefined);
77603
+ return null;
77604
+ }, /* @ts-ignore */
77605
+ ...(ngDevMode ? [{ debugName: "inspectorContent" }] : /* istanbul ignore next */ []));
77606
+ /**
77607
+ * Whether the right-docked inspector is showing the format panel (element or
77608
+ * slide properties). Drives the top-bar inspector-toggle active state.
77609
+ */
77610
+ inspectorPaneOpen = computed(() => {
77611
+ const content = this.inspectorContent();
77612
+ return content === 'element' || content === 'slide';
77613
+ }, /* @ts-ignore */
77614
+ ...(ngDevMode ? [{ debugName: "inspectorPaneOpen" }] : /* istanbul ignore next */ []));
77615
+ /** Inspector content, but null on mobile once the user has swiped it away. */
77616
+ visibleInspectorKind = computed(() => this.mobile.isMobile() && this.mobileInspectorHidden() ? null : this.inspectorContent(), /* @ts-ignore */
77617
+ ...(ngDevMode ? [{ debugName: "visibleInspectorKind" }] : /* istanbul ignore next */ []));
77618
+ /** Accessible-label translation key for the inspector host, by active content. */
77619
+ inspectorLabel = computed(() => {
77620
+ switch (this.inspectorContent()) {
77621
+ case 'accessibility':
77622
+ return 'pptx.accessibility.title';
77623
+ case 'signatures':
77624
+ return 'pptx.viewer.digitalSignatures';
77625
+ case 'comments':
77626
+ return 'pptx.toolbar.comments';
77627
+ case 'selection':
77628
+ return 'pptx.selectionPane.title';
77629
+ case 'element':
77630
+ return 'pptx.viewer.elementProperties';
77631
+ case 'slide':
77632
+ return 'pptx.viewer.slideProperties';
77633
+ default:
77634
+ return '';
76851
77635
  }
77636
+ }, /* @ts-ignore */
77637
+ ...(ngDevMode ? [{ debugName: "inspectorLabel" }] : /* istanbul ignore next */ []));
77638
+ /** Toggle a right-docked tool panel (clicking the active one closes it). */
77639
+ togglePanel(panel) {
77640
+ this.activePanel.update((current) => (current === panel ? null : panel));
77641
+ // Tapping a panel button re-opens the host even after a swipe-dismiss.
77642
+ this.mobileInspectorHidden.set(false);
76852
77643
  }
76853
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
76854
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService });
77644
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerInspectorPanelService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77645
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerInspectorPanelService });
76855
77646
  }
76856
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService, decorators: [{
77647
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerInspectorPanelService, decorators: [{
76857
77648
  type: Injectable
76858
77649
  }] });
76859
77650
 
@@ -76984,25 +77775,347 @@ class ViewerKeyboardService {
76984
77775
  break;
76985
77776
  }
76986
77777
  }
76987
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
76988
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService });
77778
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77779
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService });
77780
+ }
77781
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService, decorators: [{
77782
+ type: Injectable
77783
+ }] });
77784
+
77785
+ /**
77786
+ * viewer-mobile-sheet.service.ts: Viewer-scoped state for the mobile-only
77787
+ * chrome: which bottom-sheet ('slides' | 'menu') is open, the speaker-notes
77788
+ * strip visibility + its swipe-to-dismiss drag, and the mobile "Insert" quick
77789
+ * action (drop a text box on the active slide).
77790
+ *
77791
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the
77792
+ * accessors it alone owns (canEdit / slide count / active-slide-index) via
77793
+ * {@link bind}; the template reads the signals / invokes the handlers off the
77794
+ * injected instance directly (same pattern as `session`/`xport`).
77795
+ *
77796
+ * Provide it once on the viewer component (`providers: [ViewerMobileSheetService]`).
77797
+ */
77798
+ class ViewerMobileSheetService {
77799
+ editor = inject(EditorStateService);
77800
+ /** Open mobile bottom-sheet (slides / menu), or null. */
77801
+ mobileSheet = signal(null, /* @ts-ignore */
77802
+ ...(ngDevMode ? [{ debugName: "mobileSheet" }] : /* istanbul ignore next */ []));
77803
+ /** Speaker-notes strip visibility. */
77804
+ showNotes = signal(false, /* @ts-ignore */
77805
+ ...(ngDevMode ? [{ debugName: "showNotes" }] : /* istanbul ignore next */ []));
77806
+ /**
77807
+ * Swipe-to-dismiss drag for the notes sheet. The sheet stays in normal flow
77808
+ * (see template/CSS notes), so the drag gesture is wired here rather than
77809
+ * via a fixed-overlay dismiss.
77810
+ */
77811
+ notesDrag = createSwipeDismissDrag(() => this.showNotes.set(false));
77812
+ host = null;
77813
+ /** Wire the host accessors (called once from the component constructor). */
77814
+ bind(host) {
77815
+ this.host = host;
77816
+ }
77817
+ requireHost() {
77818
+ if (!this.host) {
77819
+ throw new Error('ViewerMobileSheetService.bind() was not called');
77820
+ }
77821
+ return this.host;
77822
+ }
77823
+ /** Toggle the speaker-notes strip. */
77824
+ toggleNotes() {
77825
+ this.showNotes.update((v) => !v);
77826
+ }
77827
+ /**
77828
+ * Mobile quick-insert: drop a text box on the active slide. Mirrors React's
77829
+ * mobile bottom-bar "Insert" slot (a text box is the most common starter
77830
+ * element on a phone; the full Insert section lives in the top-bar menu).
77831
+ */
77832
+ onMobileInsert() {
77833
+ const host = this.requireHost();
77834
+ if (!host.canEdit() || host.slideCount() === 0) {
77835
+ return;
77836
+ }
77837
+ // Close any open mobile sheet so the new element is visible on the canvas.
77838
+ this.mobileSheet.set(null);
77839
+ this.editor.addElement(host.activeSlideIndex(), newTextElement());
77840
+ }
77841
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerMobileSheetService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77842
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerMobileSheetService });
77843
+ }
77844
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerMobileSheetService, decorators: [{
77845
+ type: Injectable
77846
+ }] });
77847
+
77848
+ /**
77849
+ * viewer-presentation-mode.service.ts: Viewer-scoped state + logic for the
77850
+ * fullscreen slideshow and presenter (speaker) view overlays: their visibility
77851
+ * flags, the presenter-view elapsed-timer start time, opening/closing a
77852
+ * separate audience window, mapping a (possibly custom-show-filtered)
77853
+ * presentation-overlay index back to the full-deck active slide, and the
77854
+ * keep/discard-annotations prompt shown when a slideshow with ink on it exits.
77855
+ *
77856
+ * Extracted from {@link PowerPointViewerComponent}: the component binds the few
77857
+ * accessors it alone owns (active-slide-index get/set, the editing-id clear,
77858
+ * the source bytes for the audience hand-off, canEdit, and the keep-annotations
77859
+ * prompt trigger) via {@link bind}; the template reads the signals / invokes the
77860
+ * handlers off the injected instance directly (same pattern as `session`/`xport`).
77861
+ *
77862
+ * Provide it once on the viewer component (`providers: [ViewerPresentationModeService]`).
77863
+ */
77864
+ class ViewerPresentationModeService {
77865
+ loader = inject(LoadContentService);
77866
+ presenterWindow = inject(PresenterWindowService);
77867
+ customShowsCtl = inject(ViewerCustomShowsService);
77868
+ /** Fullscreen presentation-mode overlay visibility. */
77869
+ presenting = signal(false, /* @ts-ignore */
77870
+ ...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
77871
+ /** Presenter-view (speaker) overlay visibility. */
77872
+ presentingPresenter = signal(false, /* @ts-ignore */
77873
+ ...(ngDevMode ? [{ debugName: "presentingPresenter" }] : /* istanbul ignore next */ []));
77874
+ /** Epoch ms when presenter view started (drives the elapsed timer). */
77875
+ presenterStartTime = signal(null, /* @ts-ignore */
77876
+ ...(ngDevMode ? [{ debugName: "presenterStartTime" }] : /* istanbul ignore next */ []));
77877
+ host = null;
77878
+ /** Wire the host accessors (called once from the component constructor). */
77879
+ bind(host) {
77880
+ this.host = host;
77881
+ }
77882
+ requireHost() {
77883
+ if (!this.host) {
77884
+ throw new Error('ViewerPresentationModeService.bind() was not called');
77885
+ }
77886
+ return this.host;
77887
+ }
77888
+ /** Open the fullscreen presentation overlay from the current slide. */
77889
+ present() {
77890
+ const host = this.requireHost();
77891
+ if (host.slideCount() > 0) {
77892
+ // Deselect first so no edit chrome (selection outline / resize + rotate
77893
+ // "Adjust shape" handles) leaks over the slideshow.
77894
+ host.clearSelection();
77895
+ host.clearEditing();
77896
+ this.presenting.set(true);
77897
+ }
77898
+ }
77899
+ /**
77900
+ * Map a presentation-overlay index back to the full-deck `activeSlideIndex`.
77901
+ * The overlay's index is relative to the (possibly custom-show-filtered)
77902
+ * presentation slides, so resolve by slide id to keep the editor selection
77903
+ * correct when the show closes.
77904
+ */
77905
+ onPresentationIndexChange(index) {
77906
+ const host = this.requireHost();
77907
+ const target = this.customShowsCtl.presentationSlides()[index];
77908
+ if (!target) {
77909
+ return;
77910
+ }
77911
+ const fullIndex = this.loader.slides().findIndex((s) => s.id === target.id);
77912
+ host.setActiveSlideIndex(fullIndex >= 0 ? fullIndex : index);
77913
+ }
77914
+ /**
77915
+ * Open a separate audience tab and hand off the deck via the shared
77916
+ * IndexedDB store. Mirrors React's presenter "open audience window".
77917
+ */
77918
+ openAudienceWindow() {
77919
+ const host = this.requireHost();
77920
+ this.presenterWindow.openAudienceWindow(host.sourceContent(), host.activeSlideIndex());
77921
+ }
77922
+ /** Open the presenter (speaker) view: current+next slide, notes, timer. */
77923
+ presentPresenter() {
77924
+ const host = this.requireHost();
77925
+ if (host.slideCount() > 0) {
77926
+ this.presenterStartTime.set(Date.now());
77927
+ this.presentingPresenter.set(true);
77928
+ }
77929
+ }
77930
+ /** Close the presenter view (and any audience overlay/window it opened). */
77931
+ exitPresenter() {
77932
+ this.presentingPresenter.set(false);
77933
+ this.presenting.set(false);
77934
+ this.presenterWindow.closeAudienceWindow();
77935
+ }
77936
+ /** Presentation exited with ink on it: offer the keep/discard prompt. */
77937
+ onPresentationAnnotationsExit(map) {
77938
+ const host = this.requireHost();
77939
+ if (host.canEdit()) {
77940
+ host.promptKeepAnnotations(map);
77941
+ }
77942
+ }
77943
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerPresentationModeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77944
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerPresentationModeService });
77945
+ }
77946
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerPresentationModeService, decorators: [{
77947
+ type: Injectable
77948
+ }] });
77949
+
77950
+ /**
77951
+ * viewer-theme-gallery.service.ts: Viewer-scoped state + logic for the theme
77952
+ * gallery overlay (Design ▸ Browse Themes): its open/closed signal, the loaded
77953
+ * deck's active theme name (for the gallery's check-mark), and applying a
77954
+ * built-in preset to the whole deck.
77955
+ *
77956
+ * Extracted from {@link PowerPointViewerComponent}. Unlike most of the other
77957
+ * extracted viewer-*.service.ts controllers this one needs no `bind()`: it
77958
+ * only depends on other already-provided services (`EditorStateService`,
77959
+ * `LoadContentService`), so it injects them directly.
77960
+ *
77961
+ * Provide it once on the viewer component (`providers: [ViewerThemeGalleryService]`).
77962
+ */
77963
+ class ViewerThemeGalleryService {
77964
+ editor = inject(EditorStateService);
77965
+ loader = inject(LoadContentService);
77966
+ /** Whether the theme-gallery overlay is visible (Design → Browse Themes). */
77967
+ showThemeGallery = signal(false, /* @ts-ignore */
77968
+ ...(ngDevMode ? [{ debugName: "showThemeGallery" }] : /* istanbul ignore next */ []));
77969
+ /** The `name` property of the loaded deck's theme (for check-mark in gallery). */
77970
+ activeThemeName = computed(() => this.loader.theme()?.name, /* @ts-ignore */
77971
+ ...(ngDevMode ? [{ debugName: "activeThemeName" }] : /* istanbul ignore next */ []));
77972
+ /**
77973
+ * Apply a built-in theme preset to the whole deck.
77974
+ *
77975
+ * Mirrors Vue's `applyThemePreset()`: re-resolves slide colours via core's
77976
+ * pure `applyThemeToData`, then writes the updated slides + theme metadata
77977
+ * into `EditorStateService` as a single undoable entry. Also refreshes the
77978
+ * `loader.themeColorMap` so subsequent theme switches start from the correct
77979
+ * baseline.
77980
+ */
77981
+ applyThemePreset(preset) {
77982
+ const currentSlides = this.editor.slides();
77983
+ const result = applyThemeToData({
77984
+ slides: [...currentSlides],
77985
+ theme: this.loader.theme() ?? {},
77986
+ themeColorMap: this.loader.themeColorMap() ?? {},
77987
+ }, preset.colorScheme, preset.fontScheme, preset.name);
77988
+ // Write slides back through the editor (records undo history).
77989
+ this.editor.applyReplacement(result.slides, `Apply theme "${preset.name}"`);
77990
+ // Update the loader's theme signals so the check-mark and future switches are correct.
77991
+ this.loader.theme.set(result.theme);
77992
+ this.loader.themeColorMap.set(result.themeColorMap);
77993
+ this.showThemeGallery.set(false);
77994
+ }
77995
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerThemeGalleryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
77996
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerThemeGalleryService });
77997
+ }
77998
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerThemeGalleryService, decorators: [{
77999
+ type: Injectable
78000
+ }] });
78001
+
78002
+ /**
78003
+ * viewer-zoom.service.ts: Viewer-scoped zoom-level state for the main editing
78004
+ * canvas (not the auto-fit scale, which stays internal to `SlideCanvasComponent`).
78005
+ *
78006
+ * Extracted from {@link PowerPointViewerComponent}: the ribbon/status-bar zoom
78007
+ * controls and the pinch-to-zoom touch gesture all read/write this one signal.
78008
+ *
78009
+ * Provide it once on the viewer component (`providers: [ViewerZoomService]`).
78010
+ */
78011
+ const ZOOM_STEP = 0.1;
78012
+ const ZOOM_MIN = 0.2;
78013
+ const ZOOM_MAX = 3;
78014
+ class ViewerZoomService {
78015
+ /** Current zoom multiplier applied to the main slide canvas (1 = 100%). */
78016
+ zoom = signal(1, /* @ts-ignore */
78017
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
78018
+ /** {@link zoom} rounded to a whole percentage for display. */
78019
+ zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
78020
+ ...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
78021
+ zoomIn() {
78022
+ this.zoom.set(Math.min(ZOOM_MAX, Number((this.zoom() + ZOOM_STEP).toFixed(2))));
78023
+ }
78024
+ zoomOut() {
78025
+ this.zoom.set(Math.max(ZOOM_MIN, Number((this.zoom() - ZOOM_STEP).toFixed(2))));
78026
+ }
78027
+ zoomReset() {
78028
+ this.zoom.set(1);
78029
+ }
78030
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerZoomService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
78031
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerZoomService });
78032
+ }
78033
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerZoomService, decorators: [{
78034
+ type: Injectable
78035
+ }] });
78036
+
78037
+ /**
78038
+ * viewer-touch-gestures.service.ts: Wires the framework-agnostic touch-gesture
78039
+ * recogniser ({@link attachTouchGestures}) to the main canvas host. Mirrors
78040
+ * React's `useTouchGestures` wiring:
78041
+ * - pinch-to-zoom always updates {@link ViewerZoomService}'s zoom signal
78042
+ * (clamped to the viewer range), with `preventDefault()` on the pinch path
78043
+ * to suppress the browser's native pinch-zoom;
78044
+ * - horizontal swipe navigates slides, but only when editing is off
78045
+ * (`!canEdit()`): in edit mode single-finger gestures belong to element
78046
+ * manipulation (move/resize/rotate), so we never hijack them. The large
78047
+ * ‹ › buttons remain available for explicit navigation in all modes;
78048
+ * - long-press in edit mode opens the editor context menu at the press point
78049
+ * for the current selection (mirrors React's onLongPress path).
78050
+ *
78051
+ * Extracted from {@link PowerPointViewerComponent}: the component calls
78052
+ * {@link setup} once from its constructor with the `<main>` element accessor and
78053
+ * the mode/navigation accessors the callbacks gate on. The recogniser's swipe/
78054
+ * long-press callbacks check the live accessors, so a single attach handles
78055
+ * every mode without re-binding.
78056
+ *
78057
+ * Provide it once on the viewer component (`providers: [ViewerTouchGesturesService]`).
78058
+ */
78059
+ class ViewerTouchGesturesService {
78060
+ zoomSvc = inject(ViewerZoomService);
78061
+ destroyRef = inject(DestroyRef);
78062
+ /**
78063
+ * Attach the recogniser to `mainEl()`'s element once it is in the DOM
78064
+ * (called once from the host component's constructor).
78065
+ */
78066
+ setup(mainEl, host) {
78067
+ afterNextRender(() => {
78068
+ const el = mainEl();
78069
+ if (!el) {
78070
+ return;
78071
+ }
78072
+ const teardown = attachTouchGestures(el, {
78073
+ getScale: () => this.zoomSvc.zoom(),
78074
+ callbacks: {
78075
+ onPinchZoom: (newScale) => this.zoomSvc.zoom.set(newScale),
78076
+ onSwipe: (direction) => {
78077
+ // Edit mode: leave single-finger gestures to element manipulation.
78078
+ if (host.canEdit()) {
78079
+ return;
78080
+ }
78081
+ // direction 1 = swipe right (previous), -1 = swipe left (next).
78082
+ if (direction === 1) {
78083
+ host.goPrev();
78084
+ }
78085
+ else {
78086
+ host.goNext();
78087
+ }
78088
+ },
78089
+ onLongPress: (x, y) => {
78090
+ if (!host.canEdit() || host.presenting()) {
78091
+ return;
78092
+ }
78093
+ const selected = host.selectedElement();
78094
+ if (!selected) {
78095
+ return;
78096
+ }
78097
+ host.setContextMenuPos({ x, y });
78098
+ },
78099
+ },
78100
+ });
78101
+ this.destroyRef.onDestroy(teardown);
78102
+ });
78103
+ }
78104
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerTouchGesturesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
78105
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerTouchGesturesService });
76989
78106
  }
76990
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService, decorators: [{
78107
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerTouchGesturesService, decorators: [{
76991
78108
  type: Injectable
76992
78109
  }] });
76993
78110
 
76994
- const ZOOM_STEP = 0.1;
76995
- const ZOOM_MIN = 0.2;
76996
- const ZOOM_MAX = 3;
76997
78111
  /**
76998
78112
  * PowerPointViewerComponent: Angular port of the React `PowerPointViewer.tsx`
76999
78113
  * and Vue `PowerPointViewer.vue`.
77000
78114
  *
77001
78115
  * Top-level orchestrator that loads `.pptx` bytes and renders the slides with
77002
- * navigation and zoom. This is the viewer-first milestone of the port: the
77003
- * React component additionally composes a full editor (toolbar, inspector
77004
- * panels, dialogs, presentation mode, collaboration, export). The roadmap and
77005
- * per-area status live in `packages/angular/PORTING.md`.
78116
+ * navigation and zoom, composing the full editor (toolbar, inspector panels,
78117
+ * dialogs, presentation mode, collaboration, export) like its React and Vue
78118
+ * counterparts.
77006
78119
  *
77007
78120
  * Conventions vs. React/Vue:
77008
78121
  * - React `forwardRef` handle / Vue `defineExpose` → public {@link getContent}
@@ -77082,7 +78195,6 @@ class PowerPointViewerComponent {
77082
78195
  /** Fired when the collaboration/broadcast session stops. Mirrors React/Vue. */
77083
78196
  stopCollaboration = output();
77084
78197
  loader = inject(LoadContentService);
77085
- exportSvc = inject(ExportService);
77086
78198
  editor = inject(EditorStateService);
77087
78199
  fonts = inject(EmbeddedFontsService);
77088
78200
  collab = inject(CollaborationService);
@@ -77100,6 +78212,16 @@ class PowerPointViewerComponent {
77100
78212
  session = inject(ViewerCollaborationSessionService);
77101
78213
  formatPainter = inject(ViewerFormatPainterService);
77102
78214
  keyboard = inject(ViewerKeyboardService);
78215
+ zoomSvc = inject(ViewerZoomService);
78216
+ touchGestures = inject(ViewerTouchGesturesService);
78217
+ presentationMode = inject(ViewerPresentationModeService);
78218
+ mobileSheetSvc = inject(ViewerMobileSheetService);
78219
+ inspectorPanel = inject(ViewerInspectorPanelService);
78220
+ fileIO = inject(ViewerFileIOService);
78221
+ themeGallery = inject(ViewerThemeGalleryService);
78222
+ canvasEditing = inject(ViewerCanvasEditingService);
78223
+ collabCursor = inject(ViewerCollabCursorService);
78224
+ docProperties = inject(ViewerDocumentPropertiesService);
77103
78225
  /** Handle on the secondary-dialog host (keep-annotations prompt). */
77104
78226
  extraDialogs = viewChild(ViewerExtraDialogsComponent, /* @ts-ignore */
77105
78227
  ...(ngDevMode ? [{ debugName: "extraDialogs" }] : /* istanbul ignore next */ []));
@@ -77146,36 +78268,9 @@ class PowerPointViewerComponent {
77146
78268
  ...(ngDevMode ? [{ debugName: "activeTemplateElements" }] : /* istanbul ignore next */ []));
77147
78269
  rootStyle = computed(() => themeStyle(this.theme()), /* @ts-ignore */
77148
78270
  ...(ngDevMode ? [{ debugName: "rootStyle" }] : /* istanbul ignore next */ []));
77149
- zoom = signal(1, /* @ts-ignore */
77150
- ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
77151
- zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
77152
- ...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
77153
- /**
77154
- * Remote cursors filtered to the slide the local user is viewing, so peers'
77155
- * cursors only appear on the shared slide (mirrors React/Vue).
77156
- */
77157
- collabCursors = computed(() => presenceToCursors(this.collab.presence(), this.activeSlideIndex()), /* @ts-ignore */
77158
- ...(ngDevMode ? [{ debugName: "collabCursors" }] : /* istanbul ignore next */ []));
77159
- /** Timestamp of the last cursor broadcast (throttle gate). */
77160
- lastCursorBroadcast = 0;
77161
- /** Fullscreen presentation-mode overlay visibility. */
77162
- presenting = signal(false, /* @ts-ignore */
77163
- ...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
77164
- /** Presenter-view (speaker) overlay visibility. */
77165
- presentingPresenter = signal(false, /* @ts-ignore */
77166
- ...(ngDevMode ? [{ debugName: "presentingPresenter" }] : /* istanbul ignore next */ []));
77167
- /** Epoch ms when presenter view started (drives the elapsed timer). */
77168
- presenterStartTime = signal(null, /* @ts-ignore */
77169
- ...(ngDevMode ? [{ debugName: "presenterStartTime" }] : /* istanbul ignore next */ []));
77170
78271
  /** Slide-sorter grid overlay visibility. */
77171
78272
  showSorter = signal(false, /* @ts-ignore */
77172
78273
  ...(ngDevMode ? [{ debugName: "showSorter" }] : /* istanbul ignore next */ []));
77173
- /** Open mobile bottom-sheet (slides / menu), or null. */
77174
- mobileSheet = signal(null, /* @ts-ignore */
77175
- ...(ngDevMode ? [{ debugName: "mobileSheet" }] : /* istanbul ignore next */ []));
77176
- /** Speaker-notes strip visibility. */
77177
- showNotes = signal(false, /* @ts-ignore */
77178
- ...(ngDevMode ? [{ debugName: "showNotes" }] : /* istanbul ignore next */ []));
77179
78274
  /** Whether the left slides panel is collapsed (top-bar sidebar toggle). */
77180
78275
  slidesPanelCollapsed = signal(false, /* @ts-ignore */
77181
78276
  ...(ngDevMode ? [{ debugName: "slidesPanelCollapsed" }] : /* istanbul ignore next */ []));
@@ -77189,73 +78284,6 @@ class PowerPointViewerComponent {
77189
78284
  /** Active ink stroke width in stage pixels. */
77190
78285
  activeDrawWidth = signal(3, /* @ts-ignore */
77191
78286
  ...(ngDevMode ? [{ debugName: "activeDrawWidth" }] : /* istanbul ignore next */ []));
77192
- /** Active right-docked tool panel (comments / accessibility / selection), or null. */
77193
- activePanel = signal(null, /* @ts-ignore */
77194
- ...(ngDevMode ? [{ debugName: "activePanel" }] : /* istanbul ignore next */ []));
77195
- /**
77196
- * Which panel the single inspector host should show, applying the original
77197
- * first-match precedence (explicit tool panels → element → slide default).
77198
- * `accessibility`/`signatures` render regardless of edit mode; the rest need
77199
- * `canEdit`.
77200
- */
77201
- inspectorContent = computed(() => {
77202
- const panel = this.activePanel();
77203
- if (panel === 'accessibility') {
77204
- return 'accessibility';
77205
- }
77206
- if (panel === 'signatures') {
77207
- return 'signatures';
77208
- }
77209
- if (!this.canEdit()) {
77210
- return null;
77211
- }
77212
- if (panel === 'comments') {
77213
- return 'comments';
77214
- }
77215
- if (panel === 'selection') {
77216
- return 'selection';
77217
- }
77218
- if (this.selectedElement()) {
77219
- return 'element';
77220
- }
77221
- if (this.activeSlide()) {
77222
- return 'slide';
77223
- }
77224
- return null;
77225
- }, /* @ts-ignore */
77226
- ...(ngDevMode ? [{ debugName: "inspectorContent" }] : /* istanbul ignore next */ []));
77227
- /**
77228
- * Whether the right-docked inspector is showing the format panel (element or
77229
- * slide properties). Drives the top-bar inspector-toggle active state.
77230
- */
77231
- inspectorPaneOpen = computed(() => {
77232
- const content = this.inspectorContent();
77233
- return content === 'element' || content === 'slide';
77234
- }, /* @ts-ignore */
77235
- ...(ngDevMode ? [{ debugName: "inspectorPaneOpen" }] : /* istanbul ignore next */ []));
77236
- /** Inspector content, but null on mobile once the user has swiped it away. */
77237
- visibleInspectorKind = computed(() => this.mobile.isMobile() && this.mobileInspectorHidden() ? null : this.inspectorContent(), /* @ts-ignore */
77238
- ...(ngDevMode ? [{ debugName: "visibleInspectorKind" }] : /* istanbul ignore next */ []));
77239
- /** Accessible-label translation key for the inspector host, by active content. */
77240
- inspectorLabel = computed(() => {
77241
- switch (this.inspectorContent()) {
77242
- case 'accessibility':
77243
- return 'pptx.accessibility.title';
77244
- case 'signatures':
77245
- return 'pptx.viewer.digitalSignatures';
77246
- case 'comments':
77247
- return 'pptx.toolbar.comments';
77248
- case 'selection':
77249
- return 'pptx.selectionPane.title';
77250
- case 'element':
77251
- return 'pptx.viewer.elementProperties';
77252
- case 'slide':
77253
- return 'pptx.viewer.slideProperties';
77254
- default:
77255
- return '';
77256
- }
77257
- }, /* @ts-ignore */
77258
- ...(ngDevMode ? [{ debugName: "inspectorLabel" }] : /* istanbul ignore next */ []));
77259
78287
  /**
77260
78288
  * Which mobile bottom-bar slot is currently "active" (highlighted). The
77261
78289
  * comments panel maps to the Comments slot; an open notes strip maps to
@@ -77263,13 +78291,13 @@ class PowerPointViewerComponent {
77263
78291
  * selected the inspector (Format) is showing inline so it maps to inspector.
77264
78292
  */
77265
78293
  mobileBarSheet = computed(() => {
77266
- if (this.mobileSheet() === 'slides') {
78294
+ if (this.mobileSheetSvc.mobileSheet() === 'slides') {
77267
78295
  return 'slides';
77268
78296
  }
77269
- if (this.activePanel() === 'comments') {
78297
+ if (this.inspectorPanel.activePanel() === 'comments') {
77270
78298
  return 'comments';
77271
78299
  }
77272
- if (this.showNotes()) {
78300
+ if (this.mobileSheetSvc.showNotes()) {
77273
78301
  return 'notes';
77274
78302
  }
77275
78303
  if (this.selectedElement()) {
@@ -77278,30 +78306,9 @@ class PowerPointViewerComponent {
77278
78306
  return null;
77279
78307
  }, /* @ts-ignore */
77280
78308
  ...(ngDevMode ? [{ debugName: "mobileBarSheet" }] : /* istanbul ignore next */ []));
77281
- /** Document-properties (Info) dialog visibility. */
77282
- showProperties = signal(false, /* @ts-ignore */
77283
- ...(ngDevMode ? [{ debugName: "showProperties" }] : /* istanbul ignore next */ []));
77284
- /** Hyperlink-edit dialog visibility. */
77285
- showHyperlink = signal(false, /* @ts-ignore */
77286
- ...(ngDevMode ? [{ debugName: "showHyperlink" }] : /* istanbul ignore next */ []));
77287
- /** Local overrides applied to document properties via the Info dialog. */
77288
- coreOverride = signal({}, /* @ts-ignore */
77289
- ...(ngDevMode ? [{ debugName: "coreOverride" }] : /* istanbul ignore next */ []));
77290
78309
  /** Comments on the active slide. */
77291
78310
  activeComments = computed(() => this.activeSlide()?.comments ?? [], /* @ts-ignore */
77292
78311
  ...(ngDevMode ? [{ debugName: "activeComments" }] : /* istanbul ignore next */ []));
77293
- /** Document core properties (loaded, with any in-session edits merged in). */
77294
- coreProperties = computed(() => ({
77295
- ...(this.loader.coreProperties() ?? {}),
77296
- ...this.coreOverride(),
77297
- }), /* @ts-ignore */
77298
- ...(ngDevMode ? [{ debugName: "coreProperties" }] : /* istanbul ignore next */ []));
77299
- /** Open editor context-menu position (client coords), or null. */
77300
- contextMenuPos = signal(null, /* @ts-ignore */
77301
- ...(ngDevMode ? [{ debugName: "contextMenuPos" }] : /* istanbul ignore next */ []));
77302
- /** Id of the element being inline text-edited, or null. */
77303
- editingId = signal(null, /* @ts-ignore */
77304
- ...(ngDevMode ? [{ debugName: "editingId" }] : /* istanbul ignore next */ []));
77305
78312
  /** Whether the dot-grid overlay is visible on the editor canvas. */
77306
78313
  showGrid = signal(false, /* @ts-ignore */
77307
78314
  ...(ngDevMode ? [{ debugName: "showGrid" }] : /* istanbul ignore next */ []));
@@ -77314,15 +78321,9 @@ class PowerPointViewerComponent {
77314
78321
  /** Whether snap-to-grid is active on the editor canvas. */
77315
78322
  snapToGrid = signal(false, /* @ts-ignore */
77316
78323
  ...(ngDevMode ? [{ debugName: "snapToGrid" }] : /* istanbul ignore next */ []));
77317
- /** Whether the theme-gallery overlay is visible (Design → Browse Themes). */
77318
- showThemeGallery = signal(false, /* @ts-ignore */
77319
- ...(ngDevMode ? [{ debugName: "showThemeGallery" }] : /* istanbul ignore next */ []));
77320
78324
  /** Whether the Insert SmartArt gallery dialog is open. */
77321
78325
  showSmartArtInsert = signal(false, /* @ts-ignore */
77322
78326
  ...(ngDevMode ? [{ debugName: "showSmartArtInsert" }] : /* istanbul ignore next */ []));
77323
- /** The `name` property of the loaded deck's theme (for check-mark in gallery). */
77324
- activeThemeName = computed(() => this.loader.theme()?.name, /* @ts-ignore */
77325
- ...(ngDevMode ? [{ debugName: "activeThemeName" }] : /* istanbul ignore next */ []));
77326
78327
  /**
77327
78328
  * Stable, always-truthy key for the slide-properties form. Changes only when
77328
78329
  * the active slide changes, so the `@if` recreates (and reseeds) the
@@ -77345,13 +78346,6 @@ class PowerPointViewerComponent {
77345
78346
  null);
77346
78347
  }, /* @ts-ignore */
77347
78348
  ...(ngDevMode ? [{ debugName: "selectedElement" }] : /* istanbul ignore next */ []));
77348
- /**
77349
- * Built-in File ▸ Open override of the `content` input. The native picker
77350
- * sets this to swap the deck in place; a fresh `content` input clears it so
77351
- * external reloads always win.
77352
- */
77353
- contentOverride = signal(null, /* @ts-ignore */
77354
- ...(ngDevMode ? [{ debugName: "contentOverride" }] : /* istanbul ignore next */ []));
77355
78349
  constructor() {
77356
78350
  // Surface the `smartArt3D` opt-in to the element dispatcher via the
77357
78351
  // viewer-scoped SmartArt3DService.
@@ -77361,12 +78355,11 @@ class PowerPointViewerComponent {
77361
78355
  // A new host `content` input supersedes any in-place picked file.
77362
78356
  effect(() => {
77363
78357
  this.content();
77364
- this.contentOverride.set(null);
78358
+ this.fileIO.contentOverride.set(null);
77365
78359
  });
77366
78360
  // Load whenever the active content (picked override, else input) changes.
77367
78361
  effect(() => {
77368
- const content = this.contentOverride() ?? this.content();
77369
- void this.loader.load(content);
78362
+ void this.loader.load(this.fileIO.activeContent());
77370
78363
  });
77371
78364
  // Reset to the first slide and seed the editable deck whenever a new
77372
78365
  // presentation finishes loading.
@@ -77379,7 +78372,7 @@ class PowerPointViewerComponent {
77379
78372
  // it on mobile — tapping a shape to edit it should surface its properties.
77380
78373
  effect(() => {
77381
78374
  if (this.selectedElement()) {
77382
- this.mobileInspectorHidden.set(false);
78375
+ this.inspectorPanel.mobileInspectorHidden.set(false);
77383
78376
  }
77384
78377
  });
77385
78378
  // Emit navigation changes.
@@ -77486,7 +78479,7 @@ class PowerPointViewerComponent {
77486
78479
  getTemplateElements: () => this.editor.templateElementsBySlideId(),
77487
78480
  applyRemoteSlides: (slides) => this.editor.applyRemoteSlides(slides),
77488
78481
  canvasSize: () => this.loader.canvasSize(),
77489
- getSourceBytes: () => this.currentSourceBytes(),
78482
+ getSourceBytes: () => this.fileIO.sourceBytes(),
77490
78483
  currentSlides: () => this.editor.slides(),
77491
78484
  emitStart: (config) => this.startCollaboration.emit(config),
77492
78485
  emitStop: () => this.stopCollaboration.emit(),
@@ -77502,34 +78495,88 @@ class PowerPointViewerComponent {
77502
78495
  // on (the @HostListener stays on the component).
77503
78496
  this.keyboard.bind({
77504
78497
  canEdit: () => this.canEdit(),
77505
- presenting: () => this.presenting(),
78498
+ presenting: () => this.presentationMode.presenting(),
77506
78499
  activeSlideIndex: () => this.activeSlideIndex(),
77507
78500
  });
77508
78501
  // Attach multi-touch gestures (pinch-zoom / swipe-nav / long-press menu)
77509
- // to the canvas host once it is rendered. See setupTouchGestures().
77510
- this.setupTouchGestures();
78502
+ // to the canvas host once it is rendered.
78503
+ this.touchGestures.setup(() => this.mainEl()?.nativeElement, {
78504
+ canEdit: () => this.canEdit(),
78505
+ presenting: () => this.presentationMode.presenting(),
78506
+ selectedElement: () => this.selectedElement(),
78507
+ goPrev: () => this.goPrev(),
78508
+ goNext: () => this.goNext(),
78509
+ setContextMenuPos: (pos) => this.canvasEditing.contextMenuPos.set(pos),
78510
+ });
78511
+ // Hand the presentation-mode controller the few accessors it alone needs
78512
+ // from the component (active-slide-index get/set, editing/selection
78513
+ // clearing, the source bytes for the audience hand-off, and the
78514
+ // keep-annotations prompt trigger on the extra-dialogs host).
78515
+ this.presentationMode.bind({
78516
+ slideCount: () => this.slideCount(),
78517
+ activeSlideIndex: () => this.activeSlideIndex(),
78518
+ setActiveSlideIndex: (index) => this.activeSlideIndex.set(index),
78519
+ clearEditing: () => this.canvasEditing.editingId.set(null),
78520
+ clearSelection: () => this.editor.clearSelection(),
78521
+ sourceContent: () => this.fileIO.activeContent(),
78522
+ canEdit: () => this.canEdit(),
78523
+ promptKeepAnnotations: (map) => this.extraDialogs()?.promptKeepAnnotations(map),
78524
+ });
78525
+ // Hand the mobile-sheet controller the accessors its quick-insert action
78526
+ // needs from the component.
78527
+ this.mobileSheetSvc.bind({
78528
+ canEdit: () => this.canEdit(),
78529
+ slideCount: () => this.slideCount(),
78530
+ activeSlideIndex: () => this.activeSlideIndex(),
78531
+ });
78532
+ // Hand the inspector-panel controller the accessors its content
78533
+ // precedence needs from the component.
78534
+ this.inspectorPanel.bind({
78535
+ canEdit: () => this.canEdit(),
78536
+ selectedElement: () => this.selectedElement(),
78537
+ activeSlide: () => this.activeSlide(),
78538
+ });
78539
+ // Hand the file-IO controller the accessors it alone needs from the
78540
+ // component (canEdit, the host `content` input, the File ▸ Open override,
78541
+ // the editor's slides + template elements, and the contentChange emitter).
78542
+ this.fileIO.bind({
78543
+ canEdit: () => this.canEdit(),
78544
+ content: () => this.content(),
78545
+ onOpenFile: () => this.onOpenFile(),
78546
+ slides: () => this.editor.slides(),
78547
+ templateElementsBySlideId: () => this.editor.templateElementsBySlideId(),
78548
+ emitContentChange: (bytes) => this.contentChange.emit(bytes),
78549
+ });
78550
+ // Hand the canvas-editing controller the accessors it alone needs from the
78551
+ // component (canEdit / active-slide / active-slide-index).
78552
+ this.canvasEditing.bind({
78553
+ canEdit: () => this.canEdit(),
78554
+ activeSlide: () => this.activeSlide(),
78555
+ activeSlideIndex: () => this.activeSlideIndex(),
78556
+ });
78557
+ // Hand the collab-cursor controller the accessors it alone needs from the
78558
+ // component (the `<main>` host, zoom, canvas size, active-slide-index).
78559
+ this.collabCursor.bind({
78560
+ mainElement: () => this.mainEl()?.nativeElement,
78561
+ zoom: () => this.zoomSvc.zoom(),
78562
+ canvasSize: () => this.loader.canvasSize(),
78563
+ activeSlideIndex: () => this.activeSlideIndex(),
78564
+ });
78565
+ // Hand the document-properties controller the accessors/emitter it alone
78566
+ // needs from the component.
78567
+ this.docProperties.bind({
78568
+ canEdit: () => this.canEdit(),
78569
+ selectedElement: () => this.selectedElement(),
78570
+ activeSlideIndex: () => this.activeSlideIndex(),
78571
+ emitPropertiesChange: (patch) => this.propertiesChange.emit(patch),
78572
+ });
77511
78573
  }
77512
78574
  /**
77513
78575
  * Serialise the current presentation to `.pptx` bytes (imperative handle).
77514
78576
  * When editing, this serialises the editor's edited deck so changes persist.
77515
78577
  */
77516
78578
  async getContent() {
77517
- const data = this.canEdit()
77518
- ? await this.loader.saveSlides(buildSaveSlides(this.editor.slides(), this.editor.templateElementsBySlideId()))
77519
- : await this.loader.getContent();
77520
- // Mirror React's imperative handle: serialising the deck also notifies the
77521
- // host so listeners wired to (contentChange) receive the latest bytes.
77522
- this.contentChange.emit(data);
77523
- return data;
77524
- }
77525
- /**
77526
- * Serialise the current deck and trigger a browser download of the `.pptx`.
77527
- * Surfaced on the mobile toolbar so saving is reachable without the desktop
77528
- * ribbon's File tab.
77529
- */
77530
- async saveAsPptx() {
77531
- const bytes = await this.getContent();
77532
- this.exportSvc.savePptx(bytes, 'presentation.pptx');
78579
+ return this.fileIO.getContent();
77533
78580
  }
77534
78581
  goTo(index) {
77535
78582
  if (index < 0 || index >= this.slideCount()) {
@@ -77543,318 +78590,13 @@ class PowerPointViewerComponent {
77543
78590
  goNext() {
77544
78591
  this.goTo(this.activeSlideIndex() + 1);
77545
78592
  }
77546
- /**
77547
- * Publish the local cursor while the pointer moves over the canvas. Throttled
77548
- * to {@link BROADCAST_THROTTLE_MS}; coordinates are mapped from client space
77549
- * into unscaled slide space (dividing by zoom, matching the cursor overlay)
77550
- * and clamped to the canvas bounds.
77551
- */
77552
- onCollabPointerMove(event) {
77553
- if (!this.collab.active()) {
77554
- return;
77555
- }
77556
- const now = Date.now();
77557
- if (now - this.lastCursorBroadcast < BROADCAST_THROTTLE_MS) {
77558
- return;
77559
- }
77560
- this.lastCursorBroadcast = now;
77561
- const host = this.mainEl()?.nativeElement;
77562
- if (!host) {
77563
- return;
77564
- }
77565
- const rect = host.getBoundingClientRect();
77566
- const zoom = this.zoom() || 1;
77567
- const size = this.loader.canvasSize();
77568
- const x = clampCursorPosition((event.clientX - rect.left) / zoom, 0, size.width);
77569
- const y = clampCursorPosition((event.clientY - rect.top) / zoom, 0, size.height);
77570
- this.collab.setCursor(x, y, this.activeSlideIndex());
77571
- }
77572
- /** The loaded source `.pptx` bytes (for elected-writer write-back), if any. */
77573
- currentSourceBytes() {
77574
- const content = this.contentOverride() ?? this.content();
77575
- if (!content) {
77576
- return null;
77577
- }
77578
- return content instanceof Uint8Array ? content : new Uint8Array(content);
77579
- }
77580
- // ── Theme gallery (Design tab) ─────────────────────────────────────────────
77581
- /**
77582
- * Apply a built-in theme preset to the whole deck.
77583
- *
77584
- * Mirrors Vue's `applyThemePreset()`: re-resolves slide colours via core's
77585
- * pure `applyThemeToData`, then writes the updated slides + theme metadata
77586
- * into `EditorStateService` as a single undoable entry. Also refreshes the
77587
- * `loader.themeColorMap` so subsequent theme switches start from the correct
77588
- * baseline.
77589
- */
77590
- applyThemePreset(preset) {
77591
- const currentSlides = this.editor.slides();
77592
- const result = applyThemeToData({
77593
- slides: [...currentSlides],
77594
- theme: this.loader.theme() ?? {},
77595
- themeColorMap: this.loader.themeColorMap() ?? {},
77596
- }, preset.colorScheme, preset.fontScheme, preset.name);
77597
- // Write slides back through the editor (records undo history).
77598
- this.editor.applyReplacement(result.slides, `Apply theme "${preset.name}"`);
77599
- // Update the loader's theme signals so the check-mark and future switches are correct.
77600
- this.loader.theme.set(result.theme);
77601
- this.loader.themeColorMap.set(result.themeColorMap);
77602
- this.showThemeGallery.set(false);
77603
- }
77604
- /**
77605
- * Wire the framework-agnostic touch-gesture recogniser to the `<main>` canvas
77606
- * host once it is in the DOM. Mirrors React's `useTouchGestures` wiring:
77607
- * - pinch-to-zoom always updates the zoom signal (clamped to the viewer
77608
- * range), with `preventDefault()` on the pinch path to suppress the
77609
- * browser's native pinch-zoom;
77610
- * - horizontal swipe navigates slides, but only when editing is off
77611
- * (`!canEdit()`): in edit mode single-finger gestures belong to element
77612
- * manipulation (move/resize/rotate), so we never hijack them. The large
77613
- * ‹ › buttons remain available for explicit navigation in all modes;
77614
- * - long-press in edit mode opens the editor context menu at the press
77615
- * point for the current selection (mirrors React's onLongPress path).
77616
- *
77617
- * The recogniser's swipe/long-press callbacks check the live `canEdit()` /
77618
- * selection state, so a single attach handles every mode without re-binding.
77619
- */
77620
- setupTouchGestures() {
77621
- const destroyRef = inject(DestroyRef);
77622
- afterNextRender(() => {
77623
- const el = this.mainEl()?.nativeElement;
77624
- if (!el) {
77625
- return;
77626
- }
77627
- const teardown = attachTouchGestures(el, {
77628
- getScale: () => this.zoom(),
77629
- callbacks: {
77630
- onPinchZoom: (newScale) => this.zoom.set(newScale),
77631
- onSwipe: (direction) => {
77632
- // Edit mode: leave single-finger gestures to element manipulation.
77633
- if (this.canEdit()) {
77634
- return;
77635
- }
77636
- // direction 1 = swipe right (previous), -1 = swipe left (next).
77637
- if (direction === 1) {
77638
- this.goPrev();
77639
- }
77640
- else {
77641
- this.goNext();
77642
- }
77643
- },
77644
- onLongPress: (x, y) => {
77645
- if (!this.canEdit() || this.presenting()) {
77646
- return;
77647
- }
77648
- const selected = this.selectedElement();
77649
- if (!selected) {
77650
- return;
77651
- }
77652
- this.contextMenuPos.set({ x, y });
77653
- },
77654
- },
77655
- });
77656
- destroyRef.onDestroy(teardown);
77657
- });
77658
- }
77659
- zoomIn() {
77660
- this.zoom.set(Math.min(ZOOM_MAX, Number((this.zoom() + ZOOM_STEP).toFixed(2))));
77661
- }
77662
- zoomOut() {
77663
- this.zoom.set(Math.max(ZOOM_MIN, Number((this.zoom() - ZOOM_STEP).toFixed(2))));
77664
- }
77665
- zoomReset() {
77666
- this.zoom.set(1);
77667
- }
77668
- /** Open the fullscreen presentation overlay from the current slide. */
77669
- present() {
77670
- if (this.slideCount() > 0) {
77671
- // Deselect first so no edit chrome (selection outline / resize + rotate
77672
- // "Adjust shape" handles) leaks over the slideshow.
77673
- this.editor.clearSelection();
77674
- this.editingId.set(null);
77675
- this.presenting.set(true);
77676
- }
77677
- }
77678
- /**
77679
- * Map a presentation-overlay index back to the full-deck `activeSlideIndex`.
77680
- * The overlay's index is relative to {@link presentationSlides} (a custom show
77681
- * may filter/reorder the deck), so resolve by slide id to keep the editor
77682
- * selection correct when the show closes.
77683
- */
77684
- onPresentationIndexChange(index) {
77685
- const target = this.customShowsCtl.presentationSlides()[index];
77686
- if (!target) {
77687
- return;
77688
- }
77689
- const fullIndex = this.loader.slides().findIndex((s) => s.id === target.id);
77690
- this.activeSlideIndex.set(fullIndex >= 0 ? fullIndex : index);
77691
- }
77692
- /**
77693
- * Open a separate audience tab and hand off the deck via the shared
77694
- * IndexedDB store. Mirrors React's presenter "open audience window".
77695
- */
77696
- openAudienceWindow() {
77697
- const content = this.contentOverride() ?? this.content();
77698
- this.presenterWindow.openAudienceWindow(content, this.activeSlideIndex());
77699
- }
77700
- /** Open the presenter (speaker) view: current+next slide, notes, timer. */
77701
- presentPresenter() {
77702
- if (this.slideCount() > 0) {
77703
- this.presenterStartTime.set(Date.now());
77704
- this.presentingPresenter.set(true);
77705
- }
77706
- }
77707
- /** Close the presenter view (and any audience overlay/window it opened). */
77708
- exitPresenter() {
77709
- this.presentingPresenter.set(false);
77710
- this.presenting.set(false);
77711
- this.presenterWindow.closeAudienceWindow();
77712
- }
77713
78593
  /** Review ▸ Compare: pick a `.pptx` and diff it against the current deck. */
77714
78594
  onOpenCompare() {
77715
78595
  this.compareSvc.startCompare();
77716
78596
  }
77717
- /**
77718
- * Double-click text edit entry: equations open the equation editor instead
77719
- * of the inline text editor (mirrors React's dbl-click-to-edit-equation).
77720
- */
77721
- onTextEditStart(id) {
77722
- const element = this.activeSlide()?.elements.find((el) => el.id === id);
77723
- const segments = element && 'textSegments' in element ? element.textSegments : undefined;
77724
- const equation = segments?.find((segment) => segment.equationXml);
77725
- if (this.canEdit() && equation?.equationXml) {
77726
- this.dialogs.openEquationEdit(id, equation.equationXml);
77727
- return;
77728
- }
77729
- this.editingId.set(id);
77730
- }
77731
- /** Apply a Ctrl/Cmd+B/I/U toggle from the inline editor (undoable). */
77732
- onTextFormat(event) {
77733
- if (!this.canEdit()) {
77734
- return;
77735
- }
77736
- const element = this.activeSlide()?.elements.find((el) => el.id === event.id);
77737
- if (!element) {
77738
- return;
77739
- }
77740
- this.editor.updateElement(this.activeSlideIndex(), event.id, textStylePatch(element, event.updates));
77741
- }
77742
- /** Presentation exited with ink on it: offer the keep/discard prompt. */
77743
- onPresentationAnnotationsExit(map) {
77744
- if (this.canEdit()) {
77745
- this.extraDialogs()?.promptKeepAnnotations(map);
77746
- }
77747
- }
77748
78597
  /** Swap the deck for a restored version-history snapshot. */
77749
78598
  onRestoreVersion(bytes) {
77750
- this.contentOverride.set(bytes);
77751
- }
77752
- /** Toggle the speaker-notes strip. */
77753
- toggleNotes() {
77754
- this.showNotes.update((v) => !v);
77755
- }
77756
- /**
77757
- * File ▸ Open: host override (`onOpenFile` input) takes precedence; otherwise
77758
- * a built-in native picker loads the chosen presentation in place.
77759
- */
77760
- openFile() {
77761
- const override = this.onOpenFile();
77762
- if (override) {
77763
- override();
77764
- return;
77765
- }
77766
- void (async () => {
77767
- const picked = await openPptxFile();
77768
- if (picked) {
77769
- this.contentOverride.set(new Uint8Array(picked.buffer));
77770
- }
77771
- })();
77772
- }
77773
- // ── Mobile notes swipe-to-dismiss ─────────────────────────────────────────
77774
- // The notes sheet stays in normal flow (see template/CSS notes), so the drag
77775
- // gesture is wired here rather than via the fixed-overlay `pptx-mobile-sheet`.
77776
- /** Live downward drag offset for the notes sheet (px; 0 when idle). */
77777
- notesDragY = signal(0, /* @ts-ignore */
77778
- ...(ngDevMode ? [{ debugName: "notesDragY" }] : /* istanbul ignore next */ []));
77779
- /** True while a notes-sheet drag is in progress (disables the snap-back transition). */
77780
- notesDragging = signal(false, /* @ts-ignore */
77781
- ...(ngDevMode ? [{ debugName: "notesDragging" }] : /* istanbul ignore next */ []));
77782
- notesDragStartY = null;
77783
- onNotesPointerDown(event) {
77784
- this.notesDragStartY = event.clientY;
77785
- this.notesDragging.set(true);
77786
- event.target.setPointerCapture?.(event.pointerId);
77787
- }
77788
- onNotesPointerMove(event) {
77789
- if (this.notesDragStartY === null) {
77790
- return;
77791
- }
77792
- this.notesDragY.set(Math.max(0, event.clientY - this.notesDragStartY));
77793
- }
77794
- onNotesPointerUp(event) {
77795
- if (this.notesDragStartY === null) {
77796
- return;
77797
- }
77798
- const delta = event.clientY - this.notesDragStartY;
77799
- this.notesDragStartY = null;
77800
- this.notesDragging.set(false);
77801
- event.target.releasePointerCapture?.(event.pointerId);
77802
- // 120 px matches `pptx-mobile-sheet`'s DISMISS_THRESHOLD for consistency.
77803
- if (delta > 120) {
77804
- this.showNotes.set(false);
77805
- }
77806
- this.notesDragY.set(0);
77807
- }
77808
- // ── Mobile inspector (Format/Comments/Selection/…) swipe-to-dismiss ─────────
77809
- // The inspector host docks in-flow below the canvas on mobile (same keyboard-
77810
- // reachability reason as the notes sheet), so the swipe gesture is wired here.
77811
- /** True once the user swiped the inspector away on mobile (until reopened). */
77812
- mobileInspectorHidden = signal(false, /* @ts-ignore */
77813
- ...(ngDevMode ? [{ debugName: "mobileInspectorHidden" }] : /* istanbul ignore next */ []));
77814
- /** Live downward drag offset for the inspector host (px; 0 when idle). */
77815
- inspectorDragY = signal(0, /* @ts-ignore */
77816
- ...(ngDevMode ? [{ debugName: "inspectorDragY" }] : /* istanbul ignore next */ []));
77817
- /** True while an inspector-host drag is in progress. */
77818
- inspectorDragging = signal(false, /* @ts-ignore */
77819
- ...(ngDevMode ? [{ debugName: "inspectorDragging" }] : /* istanbul ignore next */ []));
77820
- inspectorDragStartY = null;
77821
- onInspectorPointerDown(event) {
77822
- this.inspectorDragStartY = event.clientY;
77823
- this.inspectorDragging.set(true);
77824
- event.target.setPointerCapture?.(event.pointerId);
77825
- }
77826
- onInspectorPointerMove(event) {
77827
- if (this.inspectorDragStartY === null) {
77828
- return;
77829
- }
77830
- this.inspectorDragY.set(Math.max(0, event.clientY - this.inspectorDragStartY));
77831
- }
77832
- onInspectorPointerUp(event) {
77833
- if (this.inspectorDragStartY === null) {
77834
- return;
77835
- }
77836
- const delta = event.clientY - this.inspectorDragStartY;
77837
- this.inspectorDragStartY = null;
77838
- this.inspectorDragging.set(false);
77839
- event.target.releasePointerCapture?.(event.pointerId);
77840
- if (delta > 120) {
77841
- this.mobileInspectorHidden.set(true);
77842
- this.activePanel.set(null);
77843
- }
77844
- this.inspectorDragY.set(0);
77845
- }
77846
- /**
77847
- * Mobile quick-insert: drop a text box on the active slide. Mirrors React's
77848
- * mobile bottom-bar "Insert" slot (a text box is the most common starter
77849
- * element on a phone; the full Insert section lives in the top-bar menu).
77850
- */
77851
- onMobileInsert() {
77852
- if (!this.canEdit() || this.slideCount() === 0) {
77853
- return;
77854
- }
77855
- // Close any open mobile sheet so the new element is visible on the canvas.
77856
- this.mobileSheet.set(null);
77857
- this.editor.addElement(this.activeSlideIndex(), newTextElement());
78599
+ this.fileIO.contentOverride.set(bytes);
77858
78600
  }
77859
78601
  /**
77860
78602
  * Mobile "Format" slot: surface the inspector for the current selection. The
@@ -77864,16 +78606,10 @@ class PowerPointViewerComponent {
77864
78606
  * instead).
77865
78607
  */
77866
78608
  onMobileFormat() {
77867
- this.activePanel.set(null);
77868
- this.mobileSheet.set(null);
78609
+ this.inspectorPanel.activePanel.set(null);
78610
+ this.mobileSheetSvc.mobileSheet.set(null);
77869
78611
  // Reopen the inspector if a prior swipe-down had dismissed it.
77870
- this.mobileInspectorHidden.set(false);
77871
- }
77872
- /** Toggle a right-docked tool panel (clicking the active one closes it). */
77873
- togglePanel(panel) {
77874
- this.activePanel.update((current) => (current === panel ? null : panel));
77875
- // Tapping a panel button re-opens the host even after a swipe-dismiss.
77876
- this.mobileInspectorHidden.set(false);
78612
+ this.inspectorPanel.mobileInspectorHidden.set(false);
77877
78613
  }
77878
78614
  /** Receive draw-tool state changes from the ribbon Draw tab. */
77879
78615
  onDrawToolChange(state) {
@@ -77881,21 +78617,6 @@ class PowerPointViewerComponent {
77881
78617
  this.activeDrawColor.set(state.color);
77882
78618
  this.activeDrawWidth.set(state.width);
77883
78619
  }
77884
- /** Receive a completed ink stroke and append it to the active slide. */
77885
- onInkStrokeComplete(ink) {
77886
- if (!this.canEdit()) {
77887
- return;
77888
- }
77889
- this.editor.addElement(this.activeSlideIndex(), ink);
77890
- }
77891
- /** Receive an eraser hit and delete the targeted ink element. */
77892
- onEraserHit(id) {
77893
- if (!this.canEdit()) {
77894
- return;
77895
- }
77896
- this.editor.select([id]);
77897
- this.editor.deleteSelected(this.activeSlideIndex());
77898
- }
77899
78620
  /** Append a comment to the active slide (one history entry). */
77900
78621
  onCommentAdd(text) {
77901
78622
  const next = addCommentToList(this.activeComments(), text, 'You');
@@ -77917,94 +78638,6 @@ class PowerPointViewerComponent {
77917
78638
  this.editor.updateSlide(this.activeSlideIndex(), { comments: next });
77918
78639
  }
77919
78640
  }
77920
- /**
77921
- * Persist a document-properties edit from the Info dialog. Gated on
77922
- * {@link canEdit}: viewers may inspect properties but not mutate them
77923
- * (mirrors the comments / hyperlink edit paths).
77924
- */
77925
- onPropertiesSave(patch) {
77926
- if (!this.canEdit()) {
77927
- this.showProperties.set(false);
77928
- return;
77929
- }
77930
- this.coreOverride.update((current) => ({ ...current, ...patch }));
77931
- this.propertiesChange.emit(patch);
77932
- this.showProperties.set(false);
77933
- }
77934
- /** Apply a hyperlink edit to the selected element (one history entry). */
77935
- onHyperlinkSave(patch) {
77936
- const el = this.selectedElement();
77937
- if (el) {
77938
- this.editor.updateElement(this.activeSlideIndex(), el.id, patch);
77939
- }
77940
- this.showHyperlink.set(false);
77941
- }
77942
- /**
77943
- * Handle an element press from the canvas. Additive (Shift/Ctrl) toggles
77944
- * membership; a plain press selects the element (keeping it selected if it
77945
- * already was, so a subsequent drag works).
77946
- */
77947
- onElementSelect(event) {
77948
- // The armed format painter intercepts the next element click: apply the
77949
- // copied format to the target, then disarm (no selection change).
77950
- if (this.formatPainter.active()) {
77951
- this.formatPainter.applyToTarget(event.id);
77952
- this.formatPainter.cancel();
77953
- return;
77954
- }
77955
- if (event.additive) {
77956
- this.editor.toggleSelect(event.id, true);
77957
- }
77958
- else if (!this.editor.isSelected(event.id)) {
77959
- this.editor.select([event.id]);
77960
- }
77961
- }
77962
- /** Empty-stage press: disarm the painter if armed, else clear the selection. */
77963
- onBackgroundClick() {
77964
- if (this.formatPainter.active()) {
77965
- this.formatPainter.cancel();
77966
- return;
77967
- }
77968
- this.editor.clearSelection();
77969
- }
77970
- /** Right-click: select the element under the cursor and open the menu. */
77971
- onContextMenu(event) {
77972
- if (event.id && !this.editor.isSelected(event.id)) {
77973
- this.editor.select([event.id]);
77974
- }
77975
- this.contextMenuPos.set({ x: event.x, y: event.y });
77976
- }
77977
- /** Update the active slide's background colour. */
77978
- onSlideBackground(event) {
77979
- this.editor.updateSlide(this.activeSlideIndex(), {
77980
- backgroundColor: event.target.value,
77981
- });
77982
- }
77983
- /** Update the active slide's speaker notes. */
77984
- onSlideNotes(event) {
77985
- this.editor.updateSlide(this.activeSlideIndex(), {
77986
- notes: event.target.value,
77987
- });
77988
- }
77989
- /** Update the active slide's speaker notes from the editable NotesPanel. */
77990
- onNotesUpdate(notes) {
77991
- this.editor.updateSlide(this.activeSlideIndex(), { notes });
77992
- }
77993
- // ── Selection pane handlers ────────────────────────────────────────────────
77994
- onSelectionPaneBringForward(id) {
77995
- this.editor.select([id]);
77996
- this.editor.bringSelectedForward(this.activeSlideIndex());
77997
- }
77998
- onSelectionPaneSendBackward(id) {
77999
- this.editor.select([id]);
78000
- this.editor.sendSelectedBackward(this.activeSlideIndex());
78001
- }
78002
- onToggleElementHidden(id) {
78003
- const el = this.activeSlide()?.elements.find((e) => e.id === id);
78004
- if (el) {
78005
- this.editor.updateElement(this.activeSlideIndex(), id, { hidden: !el.hidden });
78006
- }
78007
- }
78008
78641
  // ── Insert SmartArt ────────────────────────────────────────────────────────
78009
78642
  /**
78010
78643
  * Insert a new SmartArt element built from the dialog's chosen preset + item
@@ -78016,44 +78649,6 @@ class PowerPointViewerComponent {
78016
78649
  this.editor.addElement(this.activeSlideIndex(), element);
78017
78650
  this.showSmartArtInsert.set(false);
78018
78651
  }
78019
- /** Commit an inline text edit: replace the element's text (one history entry). */
78020
- onTextCommit(event) {
78021
- this.editor.updateElement(this.activeSlideIndex(), event.id, {
78022
- text: event.text,
78023
- textSegments: [],
78024
- });
78025
- this.editingId.set(null);
78026
- }
78027
- /**
78028
- * Commit a table cell's inline text edit. Finds the table element on the
78029
- * active slide, rebuilds its `tableData` with the new cell text, and patches
78030
- * it through the editor (which records undo history).
78031
- */
78032
- onTableCellCommit(event) {
78033
- if (!this.canEdit()) {
78034
- return;
78035
- }
78036
- const el = this.activeSlide()?.elements.find((e) => e.id === event.id);
78037
- if (!el || el.type !== 'table') {
78038
- return;
78039
- }
78040
- const updated = setCellText(el, event.commit.rowIndex, event.commit.colIndex, event.commit.text);
78041
- this.editor.updateElement(this.activeSlideIndex(), event.id, {
78042
- tableData: updated.tableData,
78043
- });
78044
- }
78045
- /**
78046
- * Persist a structural table change originating on the canvas (column / row
78047
- * drag-resize) as one undoable history entry.
78048
- */
78049
- onTableChange(event) {
78050
- if (!this.canEdit()) {
78051
- return;
78052
- }
78053
- this.editor.updateElement(this.activeSlideIndex(), event.id, {
78054
- tableData: event.tableData,
78055
- });
78056
- }
78057
78652
  /**
78058
78653
  * Editing keyboard shortcuts (only when `canEdit` and not typing in a
78059
78654
  * field or presenting). The decorator must live on the component; the logic
@@ -78086,8 +78681,18 @@ class PowerPointViewerComponent {
78086
78681
  ViewerFindReplaceService,
78087
78682
  ViewerCustomShowsService,
78088
78683
  ViewerCollaborationSessionService,
78684
+ ViewerCanvasEditingService,
78685
+ ViewerCollabCursorService,
78686
+ ViewerDocumentPropertiesService,
78687
+ ViewerFileIOService,
78089
78688
  ViewerFormatPainterService,
78689
+ ViewerInspectorPanelService,
78090
78690
  ViewerKeyboardService,
78691
+ ViewerMobileSheetService,
78692
+ ViewerPresentationModeService,
78693
+ ViewerThemeGalleryService,
78694
+ ViewerTouchGesturesService,
78695
+ ViewerZoomService,
78091
78696
  ], viewQueries: [{ propertyName: "extraDialogs", first: true, predicate: ViewerExtraDialogsComponent, descendants: true, isSignal: true }, { propertyName: "mainEl", first: true, predicate: ["mainEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
78092
78697
  <div class="pptx-ng-viewer" [ngClass]="class()" [ngStyle]="rootStyle()">
78093
78698
  @if (loader.loading()) {
@@ -78111,13 +78716,13 @@ class PowerPointViewerComponent {
78111
78716
  [slideCount]="slideCount()"
78112
78717
  [canEdit]="canEdit()"
78113
78718
  [selectedElement]="selectedElement()"
78114
- [zoomPercent]="zoomPercent()"
78719
+ [zoomPercent]="zoomSvc.zoomPercent()"
78115
78720
  [formatPainterActive]="formatPainter.active()"
78116
78721
  [canActivateFormatPainter]="formatPainter.canActivate()"
78117
78722
  [exporting]="xport.exporting()"
78118
78723
  [sidebarCollapsed]="slidesPanelCollapsed()"
78119
- [inspectorOpen]="inspectorPaneOpen()"
78120
- [commentsOpen]="activePanel() === 'comments'"
78724
+ [inspectorOpen]="inspectorPanel.inspectorPaneOpen()"
78725
+ [commentsOpen]="inspectorPanel.activePanel() === 'comments'"
78121
78726
  [commentCount]="activeComments().length"
78122
78727
  [findOpen]="findReplace.showFind() || findReplace.showFindReplace()"
78123
78728
  [collabConnected]="collab.connected()"
@@ -78125,31 +78730,31 @@ class PowerPointViewerComponent {
78125
78730
  (toggleSidebar)="slidesPanelCollapsed.update(v => !v)"
78126
78731
  (prev)="goPrev()"
78127
78732
  (next)="goNext()"
78128
- (zoomIn)="zoomIn()"
78129
- (zoomOut)="zoomOut()"
78130
- (zoomReset)="zoomReset()"
78733
+ (zoomIn)="zoomSvc.zoomIn()"
78734
+ (zoomOut)="zoomSvc.zoomOut()"
78735
+ (zoomReset)="zoomSvc.zoomReset()"
78131
78736
  (find)="findReplace.showFind.set(true)"
78132
- (present)="present()"
78133
- (presenter)="presentPresenter()"
78737
+ (present)="presentationMode.present()"
78738
+ (presenter)="presentationMode.presentPresenter()"
78134
78739
  (share)="session.showShare.set(true)"
78135
78740
  (broadcast)="session.showBroadcast.set(true)"
78136
- (openFile)="openFile()"
78137
- (save)="saveAsPptx()"
78138
- (info)="showProperties.set(true)"
78741
+ (openFile)="fileIO.openFile()"
78742
+ (save)="fileIO.saveAsPptx()"
78743
+ (info)="docProperties.showProperties.set(true)"
78139
78744
  (print)="print.openDialog()"
78140
- (comments)="togglePanel('comments')"
78141
- (signatures)="togglePanel('signatures')"
78142
- (a11y)="togglePanel('accessibility')"
78143
- (link)="showHyperlink.set(true)"
78745
+ (comments)="inspectorPanel.togglePanel('comments')"
78746
+ (signatures)="inspectorPanel.togglePanel('signatures')"
78747
+ (a11y)="inspectorPanel.togglePanel('accessibility')"
78748
+ (link)="docProperties.showHyperlink.set(true)"
78144
78749
  (openSorter)="showSorter.set(true)"
78145
- (toggleNotes)="toggleNotes()"
78750
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
78146
78751
  (toggleFormatPainter)="formatPainter.toggle()"
78147
78752
  (exportPng)="xport.exportPng()"
78148
78753
  (exportPdf)="xport.exportPdf()"
78149
78754
  (exportGif)="xport.exportGif()"
78150
78755
  (exportVideo)="xport.exportVideo()"
78151
78756
  (replace)="findReplace.openFindReplace()"
78152
- (toggleInspector)="activePanel.set(null)"
78757
+ (toggleInspector)="inspectorPanel.activePanel.set(null)"
78153
78758
  (drawToolChange)="onDrawToolChange($event)"
78154
78759
  [showGrid]="showGrid()"
78155
78760
  [showRulers]="showRulers()"
@@ -78161,9 +78766,9 @@ class PowerPointViewerComponent {
78161
78766
  (toggleGuides)="showGuides.update(v => !v)"
78162
78767
  (toggleSnapToGrid)="snapToGrid.update(v => !v)"
78163
78768
  (toggleEyedropper)="formatPainter.toggleEyedropper()"
78164
- [themeGalleryOpen]="showThemeGallery()"
78165
- (toggleThemeGallery)="showThemeGallery.update(v => !v)"
78166
- (toggleSelectionPane)="togglePanel('selection')"
78769
+ [themeGalleryOpen]="themeGallery.showThemeGallery()"
78770
+ (toggleThemeGallery)="themeGallery.showThemeGallery.update(v => !v)"
78771
+ (toggleSelectionPane)="inspectorPanel.togglePanel('selection')"
78167
78772
  (openCustomShows)="customShowsCtl.showDialog.set(true)"
78168
78773
  (openSmartArtDialog)="showSmartArtInsert.set(true)"
78169
78774
  (openEquationDialog)="dialogs.openEquationInsert()"
@@ -78182,12 +78787,12 @@ class PowerPointViewerComponent {
78182
78787
  [canUndo]="editor.canUndo()"
78183
78788
  [canRedo]="editor.canRedo()"
78184
78789
  [canPresent]="slideCount() > 0"
78185
- [menuOpen]="mobileSheet() === 'menu'"
78186
- (toggleMenu)="mobileSheet.set(mobileSheet() === 'menu' ? null : 'menu')"
78790
+ [menuOpen]="mobileSheetSvc.mobileSheet() === 'menu'"
78791
+ (toggleMenu)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'menu' ? null : 'menu')"
78187
78792
  (undo)="editor.undo()"
78188
78793
  (redo)="editor.redo()"
78189
- (save)="saveAsPptx()"
78190
- (present)="present()"
78794
+ (save)="fileIO.saveAsPptx()"
78795
+ (present)="presentationMode.present()"
78191
78796
  />
78192
78797
  }
78193
78798
 
@@ -78214,12 +78819,12 @@ class PowerPointViewerComponent {
78214
78819
  </nav>
78215
78820
  }
78216
78821
 
78217
- <main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
78822
+ <main class="pptx-ng-main" #mainEl (pointermove)="collabCursor.onPointerMove($event)">
78218
78823
  <pptx-slide-canvas
78219
78824
  [slide]="activeSlide()"
78220
78825
  [canvasSize]="loader.canvasSize()"
78221
78826
  [mediaDataUrls]="loader.mediaDataUrls()"
78222
- [zoom]="zoom()"
78827
+ [zoom]="zoomSvc.zoom()"
78223
78828
  [editable]="canEdit()"
78224
78829
  [selectedIds]="editor.selectedIds()"
78225
78830
  [showGrid]="showGrid()"
@@ -78232,32 +78837,32 @@ class PowerPointViewerComponent {
78232
78837
  [drawWidth]="activeDrawWidth()"
78233
78838
  [editTemplateMode]="editor.editTemplateMode()"
78234
78839
  [templateElements]="activeTemplateElements()"
78235
- (elementSelect)="onElementSelect($event)"
78236
- (backgroundClick)="onBackgroundClick()"
78840
+ (elementSelect)="canvasEditing.onElementSelect($event)"
78841
+ (backgroundClick)="canvasEditing.onBackgroundClick()"
78237
78842
  (marqueeSelect)="editor.select($event)"
78238
78843
  (transformStart)="editor.beginTransform($event.label)"
78239
78844
  (transformUpdate)="editor.applyTransform(activeSlideIndex(), $event.id, $event.box)"
78240
78845
  (rotateUpdate)="
78241
78846
  editor.applyTransform(activeSlideIndex(), $event.id, { rotation: $event.rotation })
78242
78847
  "
78243
- (contextMenu)="onContextMenu($event)"
78244
- [editingId]="editingId()"
78245
- (textEditStart)="onTextEditStart($event.id)"
78246
- (textCommit)="onTextCommit($event)"
78247
- (textCancel)="editingId.set(null)"
78248
- (textFormat)="onTextFormat($event)"
78249
- (inkStrokeComplete)="onInkStrokeComplete($event)"
78250
- (eraserHit)="onEraserHit($event)"
78251
- (cellCommit)="onTableCellCommit($event)"
78252
- (tableChange)="onTableChange($event)"
78848
+ (contextMenu)="canvasEditing.onContextMenu($event)"
78849
+ [editingId]="canvasEditing.editingId()"
78850
+ (textEditStart)="canvasEditing.onTextEditStart($event.id)"
78851
+ (textCommit)="canvasEditing.onTextCommit($event)"
78852
+ (textCancel)="canvasEditing.editingId.set(null)"
78853
+ (textFormat)="canvasEditing.onTextFormat($event)"
78854
+ (inkStrokeComplete)="canvasEditing.onInkStrokeComplete($event)"
78855
+ (eraserHit)="canvasEditing.onEraserHit($event)"
78856
+ (cellCommit)="canvasEditing.onTableCellCommit($event)"
78857
+ (tableChange)="canvasEditing.onTableChange($event)"
78253
78858
  />
78254
78859
  @if (collab.connected()) {
78255
- <pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
78860
+ <pptx-collaboration-cursors [cursors]="collabCursor.cursors()" [zoom]="zoomSvc.zoom()" />
78256
78861
  <pptx-remote-selection-overlay
78257
78862
  [presences]="collab.presence()"
78258
78863
  [elements]="activeSlide()?.elements ?? []"
78259
78864
  [activeSlideIndex]="activeSlideIndex()"
78260
- [zoom]="zoom()"
78865
+ [zoom]="zoomSvc.zoom()"
78261
78866
  />
78262
78867
  }
78263
78868
  @if (collab.active() && collab.presence().length > 0) {
@@ -78269,11 +78874,11 @@ class PowerPointViewerComponent {
78269
78874
  />
78270
78875
  </div>
78271
78876
  }
78272
- @if (showNotes() && !mobile.isMobile()) {
78877
+ @if (mobileSheetSvc.showNotes() && !mobile.isMobile()) {
78273
78878
  <aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
78274
78879
  <pptx-notes-panel
78275
78880
  [slide]="activeSlide()"
78276
- (update)="onNotesUpdate($event)"
78881
+ (update)="canvasEditing.onNotesUpdate($event)"
78277
78882
  />
78278
78883
  </aside>
78279
78884
  }
@@ -78282,25 +78887,26 @@ class PowerPointViewerComponent {
78282
78887
  <!--
78283
78888
  Single inspector host for every right-rail panel. On mobile it docks
78284
78889
  full-width below the canvas and is swipe-dismissable (the grab handle
78285
- feeds onInspectorPointerDown/Move/Up); a downward swipe past the
78286
- threshold sets mobileInspectorHidden so the user reclaims the canvas.
78890
+ feeds inspectorPanel.inspectorDrag's onPointerDown/Move/Up); a downward
78891
+ swipe past the threshold sets mobileInspectorHidden so the user
78892
+ reclaims the canvas.
78287
78893
  -->
78288
- @if (visibleInspectorKind(); as kind) {
78894
+ @if (inspectorPanel.visibleInspectorKind(); as kind) {
78289
78895
  <aside
78290
78896
  class="pptx-ng-inspector-host"
78291
- [attr.aria-label]="inspectorLabel() | translate"
78897
+ [attr.aria-label]="inspectorPanel.inspectorLabel() | translate"
78292
78898
  [style.transform]="
78293
- inspectorDragY() > 0 ? 'translateY(' + inspectorDragY() + 'px)' : null
78899
+ inspectorPanel.inspectorDrag.dragY() > 0 ? 'translateY(' + inspectorPanel.inspectorDrag.dragY() + 'px)' : null
78294
78900
  "
78295
- [style.transition]="inspectorDragging() ? 'none' : 'transform 150ms ease-out'"
78901
+ [style.transition]="inspectorPanel.inspectorDrag.dragging() ? 'none' : 'transform 150ms ease-out'"
78296
78902
  >
78297
78903
  <!-- Swipe-down-to-dismiss grab handle (mobile only; hidden on desktop). -->
78298
78904
  <div
78299
78905
  class="pptx-ng-idrawer-grab"
78300
- (pointerdown)="onInspectorPointerDown($event)"
78301
- (pointermove)="onInspectorPointerMove($event)"
78302
- (pointerup)="onInspectorPointerUp($event)"
78303
- (pointercancel)="onInspectorPointerUp($event)"
78906
+ (pointerdown)="inspectorPanel.inspectorDrag.onPointerDown($event)"
78907
+ (pointermove)="inspectorPanel.inspectorDrag.onPointerMove($event)"
78908
+ (pointerup)="inspectorPanel.inspectorDrag.onPointerUp($event)"
78909
+ (pointercancel)="inspectorPanel.inspectorDrag.onPointerUp($event)"
78304
78910
  >
78305
78911
  <div class="pptx-ng-idrawer-handle"></div>
78306
78912
  </div>
@@ -78327,9 +78933,9 @@ class PowerPointViewerComponent {
78327
78933
  [elements]="activeSlide()?.elements ?? []"
78328
78934
  [selectedIds]="editor.selectedIds()"
78329
78935
  (selectElement)="editor.select([$event])"
78330
- (bringForward)="onSelectionPaneBringForward($event)"
78331
- (sendBackward)="onSelectionPaneSendBackward($event)"
78332
- (toggleHidden)="onToggleElementHidden($event)"
78936
+ (bringForward)="canvasEditing.onSelectionPaneBringForward($event)"
78937
+ (sendBackward)="canvasEditing.onSelectionPaneSendBackward($event)"
78938
+ (toggleHidden)="canvasEditing.onToggleElementHidden($event)"
78333
78939
  />
78334
78940
  }
78335
78941
  @case ('element') {
@@ -78353,7 +78959,7 @@ class PowerPointViewerComponent {
78353
78959
  <input
78354
78960
  type="color"
78355
78961
  [attr.value]="sl.backgroundColor || '#ffffff'"
78356
- (change)="onSlideBackground($event)"
78962
+ (change)="canvasEditing.onSlideBackground($event)"
78357
78963
  />
78358
78964
  </label>
78359
78965
  <label class="pptx-ng-prop-row pptx-ng-prop-col">
@@ -78361,8 +78967,8 @@ class PowerPointViewerComponent {
78361
78967
  <textarea
78362
78968
  rows="5"
78363
78969
  [attr.placeholder]="'pptx.viewer.speakerNotesPlaceholder' | translate"
78364
- (change)="onSlideNotes($event)"
78365
- (blur)="onSlideNotes($event)"
78970
+ (change)="canvasEditing.onSlideNotes($event)"
78971
+ (blur)="canvasEditing.onSlideNotes($event)"
78366
78972
  >{{ sl.notes || '' }}</textarea
78367
78973
  >
78368
78974
  </label>
@@ -78381,17 +78987,17 @@ class PowerPointViewerComponent {
78381
78987
  [slideCount]="slideCount()"
78382
78988
  [canEdit]="canEdit()"
78383
78989
  [dirty]="editor.dirty()"
78384
- [notesOpen]="showNotes()"
78385
- [zoomPercent]="zoomPercent()"
78990
+ [notesOpen]="mobileSheetSvc.showNotes()"
78991
+ [zoomPercent]="zoomSvc.zoomPercent()"
78386
78992
  [sorterActive]="showSorter()"
78387
- [presenting]="presenting()"
78388
- (toggleNotes)="toggleNotes()"
78993
+ [presenting]="presentationMode.presenting()"
78994
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
78389
78995
  (normalView)="showSorter.set(false)"
78390
78996
  (openSorter)="showSorter.set(true)"
78391
- (slideShow)="present()"
78392
- (zoomIn)="zoomIn()"
78393
- (zoomOut)="zoomOut()"
78394
- (zoomReset)="zoomReset()"
78997
+ (slideShow)="presentationMode.present()"
78998
+ (zoomIn)="zoomSvc.zoomIn()"
78999
+ (zoomOut)="zoomSvc.zoomOut()"
79000
+ (zoomReset)="zoomSvc.zoomReset()"
78395
79001
  />
78396
79002
  }
78397
79003
  }
@@ -78407,19 +79013,19 @@ class PowerPointViewerComponent {
78407
79013
  />
78408
79014
  }
78409
79015
 
78410
- @if (presenting()) {
79016
+ @if (presentationMode.presenting()) {
78411
79017
  <pptx-presentation-overlay
78412
79018
  [slides]="customShowsCtl.presentationSlides()"
78413
79019
  [canvasSize]="loader.canvasSize()"
78414
79020
  [mediaDataUrls]="loader.mediaDataUrls()"
78415
79021
  [startIndex]="customShowsCtl.presentationStartIndex()"
78416
- (indexChange)="onPresentationIndexChange($event)"
78417
- (annotationsExit)="onPresentationAnnotationsExit($event)"
78418
- (closed)="presenting.set(false)"
79022
+ (indexChange)="presentationMode.onPresentationIndexChange($event)"
79023
+ (annotationsExit)="presentationMode.onPresentationAnnotationsExit($event)"
79024
+ (closed)="presentationMode.presenting.set(false)"
78419
79025
  />
78420
79026
  }
78421
79027
 
78422
- @if (presentingPresenter()) {
79028
+ @if (presentationMode.presentingPresenter()) {
78423
79029
  @if (mobile.isMobile()) {
78424
79030
  <!-- Single-column mobile presenter layout (phones / landscape phones). -->
78425
79031
  <pptx-mobile-presenter-view
@@ -78427,9 +79033,9 @@ class PowerPointViewerComponent {
78427
79033
  [currentSlideIndex]="activeSlideIndex()"
78428
79034
  [canvasSize]="loader.canvasSize()"
78429
79035
  [mediaDataUrls]="loader.mediaDataUrls()"
78430
- [presentationStartTime]="presenterStartTime()"
79036
+ [presentationStartTime]="presentationMode.presenterStartTime()"
78431
79037
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
78432
- (exit)="exitPresenter()"
79038
+ (exit)="presentationMode.exitPresenter()"
78433
79039
  />
78434
79040
  } @else {
78435
79041
  <pptx-presenter-view
@@ -78437,12 +79043,12 @@ class PowerPointViewerComponent {
78437
79043
  [currentSlideIndex]="activeSlideIndex()"
78438
79044
  [canvasSize]="loader.canvasSize()"
78439
79045
  [mediaDataUrls]="loader.mediaDataUrls()"
78440
- [presentationStartTime]="presenterStartTime()"
79046
+ [presentationStartTime]="presentationMode.presenterStartTime()"
78441
79047
  [isAudienceWindowOpen]="presenterWindow.isAudienceWindowOpen()"
78442
79048
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
78443
- (openAudienceWindow)="openAudienceWindow()"
79049
+ (openAudienceWindow)="presentationMode.openAudienceWindow()"
78444
79050
  (closeAudienceWindow)="presenterWindow.closeAudienceWindow()"
78445
- (exit)="exitPresenter()"
79051
+ (exit)="presentationMode.exitPresenter()"
78446
79052
  />
78447
79053
  }
78448
79054
  }
@@ -78467,27 +79073,27 @@ class PowerPointViewerComponent {
78467
79073
  />
78468
79074
  }
78469
79075
 
78470
- @if (canEdit() && contextMenuPos(); as m) {
79076
+ @if (canEdit() && canvasEditing.contextMenuPos(); as m) {
78471
79077
  <pptx-editor-context-menu
78472
79078
  [x]="m.x"
78473
79079
  [y]="m.y"
78474
79080
  [slideIndex]="activeSlideIndex()"
78475
- (closed)="contextMenuPos.set(null)"
79081
+ (closed)="canvasEditing.contextMenuPos.set(null)"
78476
79082
  />
78477
79083
  }
78478
79084
 
78479
79085
  <pptx-theme-gallery
78480
- [open]="showThemeGallery()"
78481
- [activeName]="activeThemeName()"
78482
- (applyTheme)="applyThemePreset($event)"
78483
- (close)="showThemeGallery.set(false)"
79086
+ [open]="themeGallery.showThemeGallery()"
79087
+ [activeName]="themeGallery.activeThemeName()"
79088
+ (applyTheme)="themeGallery.applyThemePreset($event)"
79089
+ (close)="themeGallery.showThemeGallery.set(false)"
78484
79090
  />
78485
79091
 
78486
79092
  <pptx-properties-dialog
78487
- [open]="showProperties()"
78488
- [properties]="coreProperties()"
78489
- (save)="onPropertiesSave($event)"
78490
- (close)="showProperties.set(false)"
79093
+ [open]="docProperties.showProperties()"
79094
+ [properties]="docProperties.coreProperties()"
79095
+ (save)="docProperties.onPropertiesSave($event)"
79096
+ (close)="docProperties.showProperties.set(false)"
78491
79097
  />
78492
79098
 
78493
79099
  <!-- Secondary dialogs / side panels (equation, set-up show, password,
@@ -78503,10 +79109,10 @@ class PowerPointViewerComponent {
78503
79109
 
78504
79110
  @if (canEdit()) {
78505
79111
  <pptx-hyperlink-dialog
78506
- [open]="showHyperlink()"
79112
+ [open]="docProperties.showHyperlink()"
78507
79113
  [element]="selectedElement()"
78508
- (save)="onHyperlinkSave($event)"
78509
- (close)="showHyperlink.set(false)"
79114
+ (save)="docProperties.onHyperlinkSave($event)"
79115
+ (close)="docProperties.showHyperlink.set(false)"
78510
79116
  />
78511
79117
  }
78512
79118
 
@@ -78577,29 +79183,29 @@ class PowerPointViewerComponent {
78577
79183
  <!-- ── Mobile chrome (narrow / touch viewports only) ─────────────── -->
78578
79184
  @if (mobile.isMobile() && !loader.loading() && !loader.error()) {
78579
79185
  <pptx-mobile-slides-sheet
78580
- [open]="mobileSheet() === 'slides'"
79186
+ [open]="mobileSheetSvc.mobileSheet() === 'slides'"
78581
79187
  [slides]="displaySlidesMut()"
78582
79188
  [canvasSize]="loader.canvasSize()"
78583
79189
  [mediaDataUrls]="loader.mediaDataUrls()"
78584
79190
  [activeIndex]="activeSlideIndex()"
78585
79191
  (jumpToSlide)="goTo($event)"
78586
- (closed)="mobileSheet.set(null)"
79192
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
78587
79193
  />
78588
79194
 
78589
79195
  <pptx-mobile-menu-sheet
78590
- [open]="mobileSheet() === 'menu'"
79196
+ [open]="mobileSheetSvc.mobileSheet() === 'menu'"
78591
79197
  [slideCount]="slideCount()"
78592
79198
  [exporting]="xport.exporting()"
78593
- [showNotes]="showNotes()"
79199
+ [showNotes]="mobileSheetSvc.showNotes()"
78594
79200
  [canEdit]="canEdit()"
78595
- (closed)="mobileSheet.set(null)"
79201
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
78596
79202
  (openFind)="findReplace.showFind.set(true)"
78597
79203
  (openSorter)="showSorter.set(true)"
78598
- (toggleNotes)="toggleNotes()"
78599
- (insertText)="onMobileInsert()"
78600
- (present)="present()"
78601
- (openFile)="openFile()"
78602
- (savePptx)="saveAsPptx()"
79204
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
79205
+ (insertText)="mobileSheetSvc.onMobileInsert()"
79206
+ (present)="presentationMode.present()"
79207
+ (openFile)="fileIO.openFile()"
79208
+ (savePptx)="fileIO.saveAsPptx()"
78603
79209
  (exportPng)="xport.exportPng()"
78604
79210
  (exportPdf)="xport.exportPdf()"
78605
79211
  (exportGif)="xport.exportGif()"
@@ -78615,24 +79221,24 @@ class PowerPointViewerComponent {
78615
79221
  the visual viewport ends up below the document on mobile (100vh layout
78616
79222
  viewport < dynamic viewport), leaving its textarea unreachable to taps.
78617
79223
  Mirrors React, where the notes panel is a flow sibling below the canvas. -->
78618
- @if (showNotes()) {
79224
+ @if (mobileSheetSvc.showNotes()) {
78619
79225
  <div
78620
79226
  class="pptx-ng-mobile-notes-sheet"
78621
- [style.transform]="notesDragY() > 0 ? 'translateY(' + notesDragY() + 'px)' : null"
78622
- [style.transition]="notesDragging() ? 'none' : 'transform 150ms ease-out'"
79227
+ [style.transform]="mobileSheetSvc.notesDrag.dragY() > 0 ? 'translateY(' + mobileSheetSvc.notesDrag.dragY() + 'px)' : null"
79228
+ [style.transition]="mobileSheetSvc.notesDrag.dragging() ? 'none' : 'transform 150ms ease-out'"
78623
79229
  >
78624
79230
  <!-- Swipe-down-to-dismiss grab handle (kept in-flow so the keyboard
78625
79231
  can't push the textarea out of reach). -->
78626
79232
  <div
78627
79233
  class="pptx-ng-mnotes-grab"
78628
- (pointerdown)="onNotesPointerDown($event)"
78629
- (pointermove)="onNotesPointerMove($event)"
78630
- (pointerup)="onNotesPointerUp($event)"
78631
- (pointercancel)="onNotesPointerUp($event)"
79234
+ (pointerdown)="mobileSheetSvc.notesDrag.onPointerDown($event)"
79235
+ (pointermove)="mobileSheetSvc.notesDrag.onPointerMove($event)"
79236
+ (pointerup)="mobileSheetSvc.notesDrag.onPointerUp($event)"
79237
+ (pointercancel)="mobileSheetSvc.notesDrag.onPointerUp($event)"
78632
79238
  >
78633
79239
  <div class="pptx-ng-mnotes-handle"></div>
78634
79240
  </div>
78635
- <pptx-notes-panel [slide]="activeSlide()" (update)="onNotesUpdate($event)" />
79241
+ <pptx-notes-panel [slide]="activeSlide()" (update)="canvasEditing.onNotesUpdate($event)" />
78636
79242
  </div>
78637
79243
  }
78638
79244
 
@@ -78646,11 +79252,11 @@ class PowerPointViewerComponent {
78646
79252
  [slideCount]="slideCount()"
78647
79253
  [commentCount]="activeComments().length"
78648
79254
  [activeSheet]="mobileBarSheet()"
78649
- (openSlides)="mobileSheet.set(mobileSheet() === 'slides' ? null : 'slides')"
78650
- (insert)="onMobileInsert()"
79255
+ (openSlides)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'slides' ? null : 'slides')"
79256
+ (insert)="mobileSheetSvc.onMobileInsert()"
78651
79257
  (openFormat)="onMobileFormat()"
78652
- (openComments)="togglePanel('comments')"
78653
- (notes)="toggleNotes()"
79258
+ (openComments)="inspectorPanel.togglePanel('comments')"
79259
+ (notes)="mobileSheetSvc.toggleNotes()"
78654
79260
  />
78655
79261
  }
78656
79262
  </div>
@@ -78681,8 +79287,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78681
79287
  ViewerFindReplaceService,
78682
79288
  ViewerCustomShowsService,
78683
79289
  ViewerCollaborationSessionService,
79290
+ ViewerCanvasEditingService,
79291
+ ViewerCollabCursorService,
79292
+ ViewerDocumentPropertiesService,
79293
+ ViewerFileIOService,
78684
79294
  ViewerFormatPainterService,
79295
+ ViewerInspectorPanelService,
78685
79296
  ViewerKeyboardService,
79297
+ ViewerMobileSheetService,
79298
+ ViewerPresentationModeService,
79299
+ ViewerThemeGalleryService,
79300
+ ViewerTouchGesturesService,
79301
+ ViewerZoomService,
78686
79302
  ],
78687
79303
  imports: [
78688
79304
  NgClass,
@@ -78747,13 +79363,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78747
79363
  [slideCount]="slideCount()"
78748
79364
  [canEdit]="canEdit()"
78749
79365
  [selectedElement]="selectedElement()"
78750
- [zoomPercent]="zoomPercent()"
79366
+ [zoomPercent]="zoomSvc.zoomPercent()"
78751
79367
  [formatPainterActive]="formatPainter.active()"
78752
79368
  [canActivateFormatPainter]="formatPainter.canActivate()"
78753
79369
  [exporting]="xport.exporting()"
78754
79370
  [sidebarCollapsed]="slidesPanelCollapsed()"
78755
- [inspectorOpen]="inspectorPaneOpen()"
78756
- [commentsOpen]="activePanel() === 'comments'"
79371
+ [inspectorOpen]="inspectorPanel.inspectorPaneOpen()"
79372
+ [commentsOpen]="inspectorPanel.activePanel() === 'comments'"
78757
79373
  [commentCount]="activeComments().length"
78758
79374
  [findOpen]="findReplace.showFind() || findReplace.showFindReplace()"
78759
79375
  [collabConnected]="collab.connected()"
@@ -78761,31 +79377,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78761
79377
  (toggleSidebar)="slidesPanelCollapsed.update(v => !v)"
78762
79378
  (prev)="goPrev()"
78763
79379
  (next)="goNext()"
78764
- (zoomIn)="zoomIn()"
78765
- (zoomOut)="zoomOut()"
78766
- (zoomReset)="zoomReset()"
79380
+ (zoomIn)="zoomSvc.zoomIn()"
79381
+ (zoomOut)="zoomSvc.zoomOut()"
79382
+ (zoomReset)="zoomSvc.zoomReset()"
78767
79383
  (find)="findReplace.showFind.set(true)"
78768
- (present)="present()"
78769
- (presenter)="presentPresenter()"
79384
+ (present)="presentationMode.present()"
79385
+ (presenter)="presentationMode.presentPresenter()"
78770
79386
  (share)="session.showShare.set(true)"
78771
79387
  (broadcast)="session.showBroadcast.set(true)"
78772
- (openFile)="openFile()"
78773
- (save)="saveAsPptx()"
78774
- (info)="showProperties.set(true)"
79388
+ (openFile)="fileIO.openFile()"
79389
+ (save)="fileIO.saveAsPptx()"
79390
+ (info)="docProperties.showProperties.set(true)"
78775
79391
  (print)="print.openDialog()"
78776
- (comments)="togglePanel('comments')"
78777
- (signatures)="togglePanel('signatures')"
78778
- (a11y)="togglePanel('accessibility')"
78779
- (link)="showHyperlink.set(true)"
79392
+ (comments)="inspectorPanel.togglePanel('comments')"
79393
+ (signatures)="inspectorPanel.togglePanel('signatures')"
79394
+ (a11y)="inspectorPanel.togglePanel('accessibility')"
79395
+ (link)="docProperties.showHyperlink.set(true)"
78780
79396
  (openSorter)="showSorter.set(true)"
78781
- (toggleNotes)="toggleNotes()"
79397
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
78782
79398
  (toggleFormatPainter)="formatPainter.toggle()"
78783
79399
  (exportPng)="xport.exportPng()"
78784
79400
  (exportPdf)="xport.exportPdf()"
78785
79401
  (exportGif)="xport.exportGif()"
78786
79402
  (exportVideo)="xport.exportVideo()"
78787
79403
  (replace)="findReplace.openFindReplace()"
78788
- (toggleInspector)="activePanel.set(null)"
79404
+ (toggleInspector)="inspectorPanel.activePanel.set(null)"
78789
79405
  (drawToolChange)="onDrawToolChange($event)"
78790
79406
  [showGrid]="showGrid()"
78791
79407
  [showRulers]="showRulers()"
@@ -78797,9 +79413,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78797
79413
  (toggleGuides)="showGuides.update(v => !v)"
78798
79414
  (toggleSnapToGrid)="snapToGrid.update(v => !v)"
78799
79415
  (toggleEyedropper)="formatPainter.toggleEyedropper()"
78800
- [themeGalleryOpen]="showThemeGallery()"
78801
- (toggleThemeGallery)="showThemeGallery.update(v => !v)"
78802
- (toggleSelectionPane)="togglePanel('selection')"
79416
+ [themeGalleryOpen]="themeGallery.showThemeGallery()"
79417
+ (toggleThemeGallery)="themeGallery.showThemeGallery.update(v => !v)"
79418
+ (toggleSelectionPane)="inspectorPanel.togglePanel('selection')"
78803
79419
  (openCustomShows)="customShowsCtl.showDialog.set(true)"
78804
79420
  (openSmartArtDialog)="showSmartArtInsert.set(true)"
78805
79421
  (openEquationDialog)="dialogs.openEquationInsert()"
@@ -78818,12 +79434,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78818
79434
  [canUndo]="editor.canUndo()"
78819
79435
  [canRedo]="editor.canRedo()"
78820
79436
  [canPresent]="slideCount() > 0"
78821
- [menuOpen]="mobileSheet() === 'menu'"
78822
- (toggleMenu)="mobileSheet.set(mobileSheet() === 'menu' ? null : 'menu')"
79437
+ [menuOpen]="mobileSheetSvc.mobileSheet() === 'menu'"
79438
+ (toggleMenu)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'menu' ? null : 'menu')"
78823
79439
  (undo)="editor.undo()"
78824
79440
  (redo)="editor.redo()"
78825
- (save)="saveAsPptx()"
78826
- (present)="present()"
79441
+ (save)="fileIO.saveAsPptx()"
79442
+ (present)="presentationMode.present()"
78827
79443
  />
78828
79444
  }
78829
79445
 
@@ -78850,12 +79466,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78850
79466
  </nav>
78851
79467
  }
78852
79468
 
78853
- <main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
79469
+ <main class="pptx-ng-main" #mainEl (pointermove)="collabCursor.onPointerMove($event)">
78854
79470
  <pptx-slide-canvas
78855
79471
  [slide]="activeSlide()"
78856
79472
  [canvasSize]="loader.canvasSize()"
78857
79473
  [mediaDataUrls]="loader.mediaDataUrls()"
78858
- [zoom]="zoom()"
79474
+ [zoom]="zoomSvc.zoom()"
78859
79475
  [editable]="canEdit()"
78860
79476
  [selectedIds]="editor.selectedIds()"
78861
79477
  [showGrid]="showGrid()"
@@ -78868,32 +79484,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78868
79484
  [drawWidth]="activeDrawWidth()"
78869
79485
  [editTemplateMode]="editor.editTemplateMode()"
78870
79486
  [templateElements]="activeTemplateElements()"
78871
- (elementSelect)="onElementSelect($event)"
78872
- (backgroundClick)="onBackgroundClick()"
79487
+ (elementSelect)="canvasEditing.onElementSelect($event)"
79488
+ (backgroundClick)="canvasEditing.onBackgroundClick()"
78873
79489
  (marqueeSelect)="editor.select($event)"
78874
79490
  (transformStart)="editor.beginTransform($event.label)"
78875
79491
  (transformUpdate)="editor.applyTransform(activeSlideIndex(), $event.id, $event.box)"
78876
79492
  (rotateUpdate)="
78877
79493
  editor.applyTransform(activeSlideIndex(), $event.id, { rotation: $event.rotation })
78878
79494
  "
78879
- (contextMenu)="onContextMenu($event)"
78880
- [editingId]="editingId()"
78881
- (textEditStart)="onTextEditStart($event.id)"
78882
- (textCommit)="onTextCommit($event)"
78883
- (textCancel)="editingId.set(null)"
78884
- (textFormat)="onTextFormat($event)"
78885
- (inkStrokeComplete)="onInkStrokeComplete($event)"
78886
- (eraserHit)="onEraserHit($event)"
78887
- (cellCommit)="onTableCellCommit($event)"
78888
- (tableChange)="onTableChange($event)"
79495
+ (contextMenu)="canvasEditing.onContextMenu($event)"
79496
+ [editingId]="canvasEditing.editingId()"
79497
+ (textEditStart)="canvasEditing.onTextEditStart($event.id)"
79498
+ (textCommit)="canvasEditing.onTextCommit($event)"
79499
+ (textCancel)="canvasEditing.editingId.set(null)"
79500
+ (textFormat)="canvasEditing.onTextFormat($event)"
79501
+ (inkStrokeComplete)="canvasEditing.onInkStrokeComplete($event)"
79502
+ (eraserHit)="canvasEditing.onEraserHit($event)"
79503
+ (cellCommit)="canvasEditing.onTableCellCommit($event)"
79504
+ (tableChange)="canvasEditing.onTableChange($event)"
78889
79505
  />
78890
79506
  @if (collab.connected()) {
78891
- <pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
79507
+ <pptx-collaboration-cursors [cursors]="collabCursor.cursors()" [zoom]="zoomSvc.zoom()" />
78892
79508
  <pptx-remote-selection-overlay
78893
79509
  [presences]="collab.presence()"
78894
79510
  [elements]="activeSlide()?.elements ?? []"
78895
79511
  [activeSlideIndex]="activeSlideIndex()"
78896
- [zoom]="zoom()"
79512
+ [zoom]="zoomSvc.zoom()"
78897
79513
  />
78898
79514
  }
78899
79515
  @if (collab.active() && collab.presence().length > 0) {
@@ -78905,11 +79521,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78905
79521
  />
78906
79522
  </div>
78907
79523
  }
78908
- @if (showNotes() && !mobile.isMobile()) {
79524
+ @if (mobileSheetSvc.showNotes() && !mobile.isMobile()) {
78909
79525
  <aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
78910
79526
  <pptx-notes-panel
78911
79527
  [slide]="activeSlide()"
78912
- (update)="onNotesUpdate($event)"
79528
+ (update)="canvasEditing.onNotesUpdate($event)"
78913
79529
  />
78914
79530
  </aside>
78915
79531
  }
@@ -78918,25 +79534,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78918
79534
  <!--
78919
79535
  Single inspector host for every right-rail panel. On mobile it docks
78920
79536
  full-width below the canvas and is swipe-dismissable (the grab handle
78921
- feeds onInspectorPointerDown/Move/Up); a downward swipe past the
78922
- threshold sets mobileInspectorHidden so the user reclaims the canvas.
79537
+ feeds inspectorPanel.inspectorDrag's onPointerDown/Move/Up); a downward
79538
+ swipe past the threshold sets mobileInspectorHidden so the user
79539
+ reclaims the canvas.
78923
79540
  -->
78924
- @if (visibleInspectorKind(); as kind) {
79541
+ @if (inspectorPanel.visibleInspectorKind(); as kind) {
78925
79542
  <aside
78926
79543
  class="pptx-ng-inspector-host"
78927
- [attr.aria-label]="inspectorLabel() | translate"
79544
+ [attr.aria-label]="inspectorPanel.inspectorLabel() | translate"
78928
79545
  [style.transform]="
78929
- inspectorDragY() > 0 ? 'translateY(' + inspectorDragY() + 'px)' : null
79546
+ inspectorPanel.inspectorDrag.dragY() > 0 ? 'translateY(' + inspectorPanel.inspectorDrag.dragY() + 'px)' : null
78930
79547
  "
78931
- [style.transition]="inspectorDragging() ? 'none' : 'transform 150ms ease-out'"
79548
+ [style.transition]="inspectorPanel.inspectorDrag.dragging() ? 'none' : 'transform 150ms ease-out'"
78932
79549
  >
78933
79550
  <!-- Swipe-down-to-dismiss grab handle (mobile only; hidden on desktop). -->
78934
79551
  <div
78935
79552
  class="pptx-ng-idrawer-grab"
78936
- (pointerdown)="onInspectorPointerDown($event)"
78937
- (pointermove)="onInspectorPointerMove($event)"
78938
- (pointerup)="onInspectorPointerUp($event)"
78939
- (pointercancel)="onInspectorPointerUp($event)"
79553
+ (pointerdown)="inspectorPanel.inspectorDrag.onPointerDown($event)"
79554
+ (pointermove)="inspectorPanel.inspectorDrag.onPointerMove($event)"
79555
+ (pointerup)="inspectorPanel.inspectorDrag.onPointerUp($event)"
79556
+ (pointercancel)="inspectorPanel.inspectorDrag.onPointerUp($event)"
78940
79557
  >
78941
79558
  <div class="pptx-ng-idrawer-handle"></div>
78942
79559
  </div>
@@ -78963,9 +79580,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78963
79580
  [elements]="activeSlide()?.elements ?? []"
78964
79581
  [selectedIds]="editor.selectedIds()"
78965
79582
  (selectElement)="editor.select([$event])"
78966
- (bringForward)="onSelectionPaneBringForward($event)"
78967
- (sendBackward)="onSelectionPaneSendBackward($event)"
78968
- (toggleHidden)="onToggleElementHidden($event)"
79583
+ (bringForward)="canvasEditing.onSelectionPaneBringForward($event)"
79584
+ (sendBackward)="canvasEditing.onSelectionPaneSendBackward($event)"
79585
+ (toggleHidden)="canvasEditing.onToggleElementHidden($event)"
78969
79586
  />
78970
79587
  }
78971
79588
  @case ('element') {
@@ -78989,7 +79606,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78989
79606
  <input
78990
79607
  type="color"
78991
79608
  [attr.value]="sl.backgroundColor || '#ffffff'"
78992
- (change)="onSlideBackground($event)"
79609
+ (change)="canvasEditing.onSlideBackground($event)"
78993
79610
  />
78994
79611
  </label>
78995
79612
  <label class="pptx-ng-prop-row pptx-ng-prop-col">
@@ -78997,8 +79614,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78997
79614
  <textarea
78998
79615
  rows="5"
78999
79616
  [attr.placeholder]="'pptx.viewer.speakerNotesPlaceholder' | translate"
79000
- (change)="onSlideNotes($event)"
79001
- (blur)="onSlideNotes($event)"
79617
+ (change)="canvasEditing.onSlideNotes($event)"
79618
+ (blur)="canvasEditing.onSlideNotes($event)"
79002
79619
  >{{ sl.notes || '' }}</textarea
79003
79620
  >
79004
79621
  </label>
@@ -79017,17 +79634,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79017
79634
  [slideCount]="slideCount()"
79018
79635
  [canEdit]="canEdit()"
79019
79636
  [dirty]="editor.dirty()"
79020
- [notesOpen]="showNotes()"
79021
- [zoomPercent]="zoomPercent()"
79637
+ [notesOpen]="mobileSheetSvc.showNotes()"
79638
+ [zoomPercent]="zoomSvc.zoomPercent()"
79022
79639
  [sorterActive]="showSorter()"
79023
- [presenting]="presenting()"
79024
- (toggleNotes)="toggleNotes()"
79640
+ [presenting]="presentationMode.presenting()"
79641
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
79025
79642
  (normalView)="showSorter.set(false)"
79026
79643
  (openSorter)="showSorter.set(true)"
79027
- (slideShow)="present()"
79028
- (zoomIn)="zoomIn()"
79029
- (zoomOut)="zoomOut()"
79030
- (zoomReset)="zoomReset()"
79644
+ (slideShow)="presentationMode.present()"
79645
+ (zoomIn)="zoomSvc.zoomIn()"
79646
+ (zoomOut)="zoomSvc.zoomOut()"
79647
+ (zoomReset)="zoomSvc.zoomReset()"
79031
79648
  />
79032
79649
  }
79033
79650
  }
@@ -79043,19 +79660,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79043
79660
  />
79044
79661
  }
79045
79662
 
79046
- @if (presenting()) {
79663
+ @if (presentationMode.presenting()) {
79047
79664
  <pptx-presentation-overlay
79048
79665
  [slides]="customShowsCtl.presentationSlides()"
79049
79666
  [canvasSize]="loader.canvasSize()"
79050
79667
  [mediaDataUrls]="loader.mediaDataUrls()"
79051
79668
  [startIndex]="customShowsCtl.presentationStartIndex()"
79052
- (indexChange)="onPresentationIndexChange($event)"
79053
- (annotationsExit)="onPresentationAnnotationsExit($event)"
79054
- (closed)="presenting.set(false)"
79669
+ (indexChange)="presentationMode.onPresentationIndexChange($event)"
79670
+ (annotationsExit)="presentationMode.onPresentationAnnotationsExit($event)"
79671
+ (closed)="presentationMode.presenting.set(false)"
79055
79672
  />
79056
79673
  }
79057
79674
 
79058
- @if (presentingPresenter()) {
79675
+ @if (presentationMode.presentingPresenter()) {
79059
79676
  @if (mobile.isMobile()) {
79060
79677
  <!-- Single-column mobile presenter layout (phones / landscape phones). -->
79061
79678
  <pptx-mobile-presenter-view
@@ -79063,9 +79680,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79063
79680
  [currentSlideIndex]="activeSlideIndex()"
79064
79681
  [canvasSize]="loader.canvasSize()"
79065
79682
  [mediaDataUrls]="loader.mediaDataUrls()"
79066
- [presentationStartTime]="presenterStartTime()"
79683
+ [presentationStartTime]="presentationMode.presenterStartTime()"
79067
79684
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
79068
- (exit)="exitPresenter()"
79685
+ (exit)="presentationMode.exitPresenter()"
79069
79686
  />
79070
79687
  } @else {
79071
79688
  <pptx-presenter-view
@@ -79073,12 +79690,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79073
79690
  [currentSlideIndex]="activeSlideIndex()"
79074
79691
  [canvasSize]="loader.canvasSize()"
79075
79692
  [mediaDataUrls]="loader.mediaDataUrls()"
79076
- [presentationStartTime]="presenterStartTime()"
79693
+ [presentationStartTime]="presentationMode.presenterStartTime()"
79077
79694
  [isAudienceWindowOpen]="presenterWindow.isAudienceWindowOpen()"
79078
79695
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
79079
- (openAudienceWindow)="openAudienceWindow()"
79696
+ (openAudienceWindow)="presentationMode.openAudienceWindow()"
79080
79697
  (closeAudienceWindow)="presenterWindow.closeAudienceWindow()"
79081
- (exit)="exitPresenter()"
79698
+ (exit)="presentationMode.exitPresenter()"
79082
79699
  />
79083
79700
  }
79084
79701
  }
@@ -79103,27 +79720,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79103
79720
  />
79104
79721
  }
79105
79722
 
79106
- @if (canEdit() && contextMenuPos(); as m) {
79723
+ @if (canEdit() && canvasEditing.contextMenuPos(); as m) {
79107
79724
  <pptx-editor-context-menu
79108
79725
  [x]="m.x"
79109
79726
  [y]="m.y"
79110
79727
  [slideIndex]="activeSlideIndex()"
79111
- (closed)="contextMenuPos.set(null)"
79728
+ (closed)="canvasEditing.contextMenuPos.set(null)"
79112
79729
  />
79113
79730
  }
79114
79731
 
79115
79732
  <pptx-theme-gallery
79116
- [open]="showThemeGallery()"
79117
- [activeName]="activeThemeName()"
79118
- (applyTheme)="applyThemePreset($event)"
79119
- (close)="showThemeGallery.set(false)"
79733
+ [open]="themeGallery.showThemeGallery()"
79734
+ [activeName]="themeGallery.activeThemeName()"
79735
+ (applyTheme)="themeGallery.applyThemePreset($event)"
79736
+ (close)="themeGallery.showThemeGallery.set(false)"
79120
79737
  />
79121
79738
 
79122
79739
  <pptx-properties-dialog
79123
- [open]="showProperties()"
79124
- [properties]="coreProperties()"
79125
- (save)="onPropertiesSave($event)"
79126
- (close)="showProperties.set(false)"
79740
+ [open]="docProperties.showProperties()"
79741
+ [properties]="docProperties.coreProperties()"
79742
+ (save)="docProperties.onPropertiesSave($event)"
79743
+ (close)="docProperties.showProperties.set(false)"
79127
79744
  />
79128
79745
 
79129
79746
  <!-- Secondary dialogs / side panels (equation, set-up show, password,
@@ -79139,10 +79756,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79139
79756
 
79140
79757
  @if (canEdit()) {
79141
79758
  <pptx-hyperlink-dialog
79142
- [open]="showHyperlink()"
79759
+ [open]="docProperties.showHyperlink()"
79143
79760
  [element]="selectedElement()"
79144
- (save)="onHyperlinkSave($event)"
79145
- (close)="showHyperlink.set(false)"
79761
+ (save)="docProperties.onHyperlinkSave($event)"
79762
+ (close)="docProperties.showHyperlink.set(false)"
79146
79763
  />
79147
79764
  }
79148
79765
 
@@ -79213,29 +79830,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79213
79830
  <!-- ── Mobile chrome (narrow / touch viewports only) ─────────────── -->
79214
79831
  @if (mobile.isMobile() && !loader.loading() && !loader.error()) {
79215
79832
  <pptx-mobile-slides-sheet
79216
- [open]="mobileSheet() === 'slides'"
79833
+ [open]="mobileSheetSvc.mobileSheet() === 'slides'"
79217
79834
  [slides]="displaySlidesMut()"
79218
79835
  [canvasSize]="loader.canvasSize()"
79219
79836
  [mediaDataUrls]="loader.mediaDataUrls()"
79220
79837
  [activeIndex]="activeSlideIndex()"
79221
79838
  (jumpToSlide)="goTo($event)"
79222
- (closed)="mobileSheet.set(null)"
79839
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
79223
79840
  />
79224
79841
 
79225
79842
  <pptx-mobile-menu-sheet
79226
- [open]="mobileSheet() === 'menu'"
79843
+ [open]="mobileSheetSvc.mobileSheet() === 'menu'"
79227
79844
  [slideCount]="slideCount()"
79228
79845
  [exporting]="xport.exporting()"
79229
- [showNotes]="showNotes()"
79846
+ [showNotes]="mobileSheetSvc.showNotes()"
79230
79847
  [canEdit]="canEdit()"
79231
- (closed)="mobileSheet.set(null)"
79848
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
79232
79849
  (openFind)="findReplace.showFind.set(true)"
79233
79850
  (openSorter)="showSorter.set(true)"
79234
- (toggleNotes)="toggleNotes()"
79235
- (insertText)="onMobileInsert()"
79236
- (present)="present()"
79237
- (openFile)="openFile()"
79238
- (savePptx)="saveAsPptx()"
79851
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
79852
+ (insertText)="mobileSheetSvc.onMobileInsert()"
79853
+ (present)="presentationMode.present()"
79854
+ (openFile)="fileIO.openFile()"
79855
+ (savePptx)="fileIO.saveAsPptx()"
79239
79856
  (exportPng)="xport.exportPng()"
79240
79857
  (exportPdf)="xport.exportPdf()"
79241
79858
  (exportGif)="xport.exportGif()"
@@ -79251,24 +79868,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79251
79868
  the visual viewport ends up below the document on mobile (100vh layout
79252
79869
  viewport < dynamic viewport), leaving its textarea unreachable to taps.
79253
79870
  Mirrors React, where the notes panel is a flow sibling below the canvas. -->
79254
- @if (showNotes()) {
79871
+ @if (mobileSheetSvc.showNotes()) {
79255
79872
  <div
79256
79873
  class="pptx-ng-mobile-notes-sheet"
79257
- [style.transform]="notesDragY() > 0 ? 'translateY(' + notesDragY() + 'px)' : null"
79258
- [style.transition]="notesDragging() ? 'none' : 'transform 150ms ease-out'"
79874
+ [style.transform]="mobileSheetSvc.notesDrag.dragY() > 0 ? 'translateY(' + mobileSheetSvc.notesDrag.dragY() + 'px)' : null"
79875
+ [style.transition]="mobileSheetSvc.notesDrag.dragging() ? 'none' : 'transform 150ms ease-out'"
79259
79876
  >
79260
79877
  <!-- Swipe-down-to-dismiss grab handle (kept in-flow so the keyboard
79261
79878
  can't push the textarea out of reach). -->
79262
79879
  <div
79263
79880
  class="pptx-ng-mnotes-grab"
79264
- (pointerdown)="onNotesPointerDown($event)"
79265
- (pointermove)="onNotesPointerMove($event)"
79266
- (pointerup)="onNotesPointerUp($event)"
79267
- (pointercancel)="onNotesPointerUp($event)"
79881
+ (pointerdown)="mobileSheetSvc.notesDrag.onPointerDown($event)"
79882
+ (pointermove)="mobileSheetSvc.notesDrag.onPointerMove($event)"
79883
+ (pointerup)="mobileSheetSvc.notesDrag.onPointerUp($event)"
79884
+ (pointercancel)="mobileSheetSvc.notesDrag.onPointerUp($event)"
79268
79885
  >
79269
79886
  <div class="pptx-ng-mnotes-handle"></div>
79270
79887
  </div>
79271
- <pptx-notes-panel [slide]="activeSlide()" (update)="onNotesUpdate($event)" />
79888
+ <pptx-notes-panel [slide]="activeSlide()" (update)="canvasEditing.onNotesUpdate($event)" />
79272
79889
  </div>
79273
79890
  }
79274
79891
 
@@ -79282,11 +79899,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79282
79899
  [slideCount]="slideCount()"
79283
79900
  [commentCount]="activeComments().length"
79284
79901
  [activeSheet]="mobileBarSheet()"
79285
- (openSlides)="mobileSheet.set(mobileSheet() === 'slides' ? null : 'slides')"
79286
- (insert)="onMobileInsert()"
79902
+ (openSlides)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'slides' ? null : 'slides')"
79903
+ (insert)="mobileSheetSvc.onMobileInsert()"
79287
79904
  (openFormat)="onMobileFormat()"
79288
- (openComments)="togglePanel('comments')"
79289
- (notes)="toggleNotes()"
79905
+ (openComments)="inspectorPanel.togglePanel('comments')"
79906
+ (notes)="mobileSheetSvc.toggleNotes()"
79290
79907
  />
79291
79908
  }
79292
79909
  </div>