pptx-angular-viewer 1.1.63 → 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.
@@ -41493,15 +41493,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
41493
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"] }]
41494
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"] }] } });
41495
41495
 
41496
- /**
41497
- * Thin re-export shim → vendored `pptx-viewer-shared`
41498
- * (`render/inspector-helpers`).
41499
- *
41500
- * The pure inspector-panel value readers + shapeStyle/textStyle patch builders
41501
- * were extracted to shared and are consumed by every binding. This shim
41502
- * preserves the historical Angular import surface.
41503
- */
41504
-
41505
41496
  /**
41506
41497
  * animation-author-helpers.ts: Angular shim over the shared element-animation
41507
41498
  * authoring model.
@@ -46562,6 +46553,15 @@ function stringFromEvent$2(event) {
46562
46553
  return val.length > 0 ? val : null;
46563
46554
  }
46564
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
+
46565
46565
  /**
46566
46566
  * smart-art-node-style-helpers.ts: pure logic for per-node SmartArt style
46567
46567
  * editing + node-count boundary constraints in the Angular inspector.
@@ -51388,6 +51388,72 @@ function resolvePresenterNotes(slide) {
51388
51388
  };
51389
51389
  }
51390
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
+
51391
51457
  /**
51392
51458
  * Pure geometry for interactive element drag/resize.
51393
51459
  *
@@ -58088,6 +58154,372 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
58088
58154
  }]
58089
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"] }] } });
58090
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
+
58091
58523
  /**
58092
58524
  * Thin re-export shim -> vendored `pptx-viewer-shared`.
58093
58525
  *
@@ -58130,36 +58562,6 @@ function isViewportBackgroundPressTarget(target, currentTarget) {
58130
58562
 
58131
58563
  /** Pixels (screen-space) a pointer must move before a click becomes a drag. */
58132
58564
  const DRAG_THRESHOLD = 3;
58133
- // ── Ruler constants ───────────────────────────────────────────────────────────
58134
- /** Height/width (px) of the ruler strips: mirrors React's RULER_THICKNESS. */
58135
- const RULER_THICKNESS = 20;
58136
- /** Pixels per inch on the slide canvas (PPTX slides are 10" wide = 960 px). */
58137
- const SLIDE_PX_PER_INCH = 96;
58138
- /**
58139
- * Generate ruler tick marks for a given slide dimension and scale.
58140
- * Produces ticks every 1/4 inch (minor) and every inch (major).
58141
- */
58142
- function generateRulerTicks(slidePx, scale) {
58143
- const scaledLength = slidePx * scale;
58144
- const quarterInchPx = (SLIDE_PX_PER_INCH / 4) * scale;
58145
- if (quarterInchPx < 2) {
58146
- return [];
58147
- }
58148
- const ticks = [];
58149
- let pos = 0;
58150
- let inchIndex = 0;
58151
- while (pos <= scaledLength + 0.5) {
58152
- const isMajor = inchIndex % 4 === 0;
58153
- ticks.push({
58154
- position: pos,
58155
- isMajor,
58156
- label: isMajor && inchIndex > 0 ? String(inchIndex / 4) : null,
58157
- });
58158
- pos += quarterInchPx;
58159
- inchIndex++;
58160
- }
58161
- return ticks;
58162
- }
58163
58565
  /** Handle size in screen pixels (fine pointer: mouse/trackpad). */
58164
58566
  const HANDLE_SCREEN_PX_FINE = 9;
58165
58567
  /** Handle size in screen pixels (coarse pointer: touch); larger hit target. */
@@ -58320,8 +58722,6 @@ class SlideCanvasComponent {
58320
58722
  tableChange = output();
58321
58723
  drag = null;
58322
58724
  editCancelled = false;
58323
- /** Active guide-drag state (id + axis only), or null when nothing is being dragged. */
58324
- guideDrag = null;
58325
58725
  marquee = null;
58326
58726
  /** Live marquee rectangle (stage coords) while rubber-band selecting. */
58327
58727
  marqueeRect = signal(null, /* @ts-ignore */
@@ -58329,42 +58729,24 @@ class SlideCanvasComponent {
58329
58729
  /** Live alignment-snap guide lines (stage coords) during a move. */
58330
58730
  snapGuides = signal([], /* @ts-ignore */
58331
58731
  ...(ngDevMode ? [{ debugName: "snapGuides" }] : /* istanbul ignore next */ []));
58332
- /**
58333
- * User-created guide lines (dragged from rulers or added from toolbar).
58334
- * axis:'x' vertical line at x=pos; axis:'y' → horizontal line at y=pos.
58335
- */
58336
- rulerGuides = signal([], /* @ts-ignore */
58337
- ...(ngDevMode ? [{ debugName: "rulerGuides" }] : /* istanbul ignore next */ []));
58338
- // ── Ink drawing state ─────────────────────────────────────────────────────
58339
- /** Accumulated points for the stroke currently being drawn. */
58340
- inkPoints = [];
58341
- /** Whether a freehand stroke is in progress. Signal for template reactivity. */
58342
- inkActiveSignal = signal(false, /* @ts-ignore */
58343
- ...(ngDevMode ? [{ debugName: "inkActiveSignal" }] : /* istanbul ignore next */ []));
58344
- /** SVG path `d` for the live stroke preview (updated on every pointer move). */
58345
- liveInkPath = signal('', /* @ts-ignore */
58346
- ...(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);
58347
58736
  textEditor = viewChild('textEditor', /* @ts-ignore */
58348
58737
  ...(ngDevMode ? [{ debugName: "textEditor" }] : /* istanbul ignore next */ []));
58349
58738
  stageRef = viewChild('stage', /* @ts-ignore */
58350
58739
  ...(ngDevMode ? [{ debugName: "stageRef" }] : /* istanbul ignore next */ []));
58351
58740
  viewportRef = viewChild('viewport', /* @ts-ignore */
58352
58741
  ...(ngDevMode ? [{ debugName: "viewportRef" }] : /* istanbul ignore next */ []));
58353
- /**
58354
- * Auto-fit scale (≤ 1): how much the fixed-size slide must shrink to fit the
58355
- * scroll viewport. The authored slide is e.g. 1280×720, which overflows a
58356
- * phone; without this it renders off-screen at `zoom=1`. Mirrors the React
58357
- * viewer's `fitScale * scale` model (useZoomViewport.ts) so "100%" means "fit
58358
- * to viewport". Measured from the viewport via ResizeObserver below.
58359
- */
58360
- fitScale = signal(1, /* @ts-ignore */
58361
- ...(ngDevMode ? [{ debugName: "fitScale" }] : /* istanbul ignore next */ []));
58742
+ /** Per-instance auto-fit scale measurement (see {@link CanvasFitService}). */
58743
+ canvasFit = inject(CanvasFitService);
58362
58744
  /**
58363
58745
  * The on-screen scale used for ALL rendering and pointer→stage coordinate
58364
58746
  * math: the user's zoom folded with the auto-fit. The parent keeps showing the
58365
58747
  * raw user zoom as the percentage, so this stays internal to the canvas.
58366
58748
  */
58367
- effectiveScale = computed(() => this.fitScale() * this.zoom(), /* @ts-ignore */
58749
+ effectiveScale = computed(() => this.canvasFit.fitScale() * this.zoom(), /* @ts-ignore */
58368
58750
  ...(ngDevMode ? [{ debugName: "effectiveScale" }] : /* istanbul ignore next */ []));
58369
58751
  /** The editing id we've already initialised the textarea for, to avoid re-seeding its value mid-edit. */
58370
58752
  seededEditId = null;
@@ -58397,48 +58779,50 @@ class SlideCanvasComponent {
58397
58779
  const end = editor.nativeElement.value.length;
58398
58780
  editor.nativeElement.setSelectionRange(end, end);
58399
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
+ });
58400
58789
  // Re-fit whenever the authored slide size changes (e.g. switching decks).
58401
58790
  effect(() => {
58402
58791
  this.canvasSize();
58403
- this.recomputeFit();
58792
+ this.canvasFit.recompute();
58404
58793
  });
58405
58794
  // Observe the viewport so the slide re-fits on container resize / rotation.
58406
58795
  const destroyRef = inject(DestroyRef);
58407
58796
  afterNextRender(() => {
58408
- this.recomputeFit();
58797
+ this.canvasFit.recompute();
58409
58798
  const el = this.viewportRef()?.nativeElement;
58410
58799
  if (typeof ResizeObserver !== 'undefined' && el) {
58411
- const observer = new ResizeObserver(() => this.recomputeFit());
58800
+ const observer = new ResizeObserver(() => this.canvasFit.recompute());
58412
58801
  observer.observe(el);
58413
58802
  destroyRef.onDestroy(() => observer.disconnect());
58414
58803
  }
58415
58804
  });
58416
- }
58417
- /**
58418
- * Compute the largest scale (≤ 1) at which the whole slide fits the viewport,
58419
- * reserving the 1rem gutter + drop shadow. Sets fitScale to 1 when unmeasured.
58420
- */
58421
- recomputeFit() {
58422
- // Thumbnail consumers manage their own scale via `zoom`; keep fit at 1 so
58423
- // the two scales don't compound.
58424
- if (!this.autoFit()) {
58425
- this.fitScale.set(1);
58426
- return;
58427
- }
58428
- const el = this.viewportRef()?.nativeElement;
58429
- const size = this.canvasSize();
58430
- if (!el || !size.width || !size.height) {
58431
- this.fitScale.set(1);
58432
- return;
58433
- }
58434
- const availW = Math.max(el.clientWidth - 16, 0);
58435
- const availH = Math.max(el.clientHeight - 32, 0);
58436
- if (!availW || !availH) {
58437
- this.fitScale.set(1);
58438
- return;
58439
- }
58440
- const fit = Math.min(availW / size.width, availH / size.height, 1);
58441
- 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
+ });
58442
58826
  }
58443
58827
  elements = computed(() => this.slide()?.elements ?? [], /* @ts-ignore */
58444
58828
  ...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
@@ -58471,62 +58855,16 @@ class SlideCanvasComponent {
58471
58855
  .map((e) => ({ x: e.x, y: e.y, width: e.width, height: e.height })), /* @ts-ignore */
58472
58856
  ...(ngDevMode ? [{ debugName: "connectorObstacles" }] : /* istanbul ignore next */ []));
58473
58857
  /** Bounding boxes (stage coords) for the selected elements. */
58474
- selectionBoxes = computed(() => {
58475
- const selected = new Set(this.selectedIds());
58476
- if (selected.size === 0) {
58477
- return [];
58478
- }
58479
- return this.allElements()
58480
- .filter((el) => selected.has(el.id))
58481
- .map((el) => ({ id: el.id, x: el.x, y: el.y, width: el.width, height: el.height }));
58482
- }, /* @ts-ignore */
58858
+ selectionBoxes = computed(() => computeSelectionBoxes(this.allElements(), this.selectedIds()), /* @ts-ignore */
58483
58859
  ...(ngDevMode ? [{ debugName: "selectionBoxes" }] : /* istanbul ignore next */ []));
58484
58860
  /** The single selected element's box, or null when 0 or >1 are selected. */
58485
- singleSelected = computed(() => {
58486
- const ids = this.selectedIds();
58487
- if (ids.length !== 1) {
58488
- return null;
58489
- }
58490
- const el = this.allElements().find((e) => e.id === ids[0]);
58491
- return el ? { id: el.id, x: el.x, y: el.y, width: el.width, height: el.height } : null;
58492
- }, /* @ts-ignore */
58861
+ singleSelected = computed(() => computeSingleSelected(this.allElements(), this.selectedIds()), /* @ts-ignore */
58493
58862
  ...(ngDevMode ? [{ debugName: "singleSelected" }] : /* istanbul ignore next */ []));
58494
58863
  /** Resize-handle render boxes (stage coords) for the single selection. */
58495
- handleBoxes = computed(() => {
58496
- if (!this.editable()) {
58497
- return [];
58498
- }
58499
- const box = this.singleSelected();
58500
- if (!box) {
58501
- return [];
58502
- }
58503
- const size = HANDLE_SCREEN_PX / (this.effectiveScale() || 1);
58504
- return RESIZE_HANDLES.map((handle) => {
58505
- const { fx, fy } = handleAnchor(handle);
58506
- return {
58507
- handle,
58508
- left: box.x + fx * box.width - size / 2,
58509
- top: box.y + fy * box.height - size / 2,
58510
- size,
58511
- cursor: handleCursor(handle),
58512
- };
58513
- });
58514
- }, /* @ts-ignore */
58864
+ handleBoxes = computed(() => computeHandleBoxes(this.singleSelected(), this.editable(), HANDLE_SCREEN_PX, this.effectiveScale()), /* @ts-ignore */
58515
58865
  ...(ngDevMode ? [{ debugName: "handleBoxes" }] : /* istanbul ignore next */ []));
58516
58866
  /** Rotation-handle box (stage coords) above the single selection, or null. */
58517
- rotateHandle = computed(() => {
58518
- if (!this.editable()) {
58519
- return null;
58520
- }
58521
- const box = this.singleSelected();
58522
- if (!box) {
58523
- return null;
58524
- }
58525
- const zoom = this.effectiveScale() || 1;
58526
- const size = HANDLE_SCREEN_PX / zoom;
58527
- const offset = 24 / zoom;
58528
- return { left: box.x + box.width / 2 - size / 2, top: box.y - offset - size / 2, size };
58529
- }, /* @ts-ignore */
58867
+ rotateHandle = computed(() => computeCornerHandle(this.singleSelected(), this.editable(), HANDLE_SCREEN_PX, 24, this.effectiveScale(), 'top-center'), /* @ts-ignore */
58530
58868
  ...(ngDevMode ? [{ debugName: "rotateHandle" }] : /* istanbul ignore next */ []));
58531
58869
  /**
58532
58870
  * Shape-adjustment-handle box (stage coords) for the single selection, or
@@ -58534,38 +58872,14 @@ class SlideCanvasComponent {
58534
58872
  * resize/rotate handles. Selection-only + editable-only, so it vanishes in
58535
58873
  * presentation alongside the rest of the edit chrome.
58536
58874
  */
58537
- adjustHandle = computed(() => {
58538
- if (!this.editable()) {
58539
- return null;
58540
- }
58541
- const box = this.singleSelected();
58542
- if (!box) {
58543
- return null;
58544
- }
58545
- const zoom = this.effectiveScale() || 1;
58546
- const size = HANDLE_SCREEN_PX / zoom;
58547
- const offset = 16 / zoom;
58548
- return { left: box.x - offset - size / 2, top: box.y - offset - size / 2, size };
58549
- }, /* @ts-ignore */
58875
+ adjustHandle = computed(() => computeCornerHandle(this.singleSelected(), this.editable(), HANDLE_SCREEN_PX, 16, this.effectiveScale(), 'top-left'), /* @ts-ignore */
58550
58876
  ...(ngDevMode ? [{ debugName: "adjustHandle" }] : /* istanbul ignore next */ []));
58551
58877
  /**
58552
58878
  * Resolve the id of the interactive element under a pointer target, or null.
58553
- * An element host carries `data-element-id`, but template (master/layout)
58554
- * elements are only interactive while editTemplateMode is on; when off they
58555
- * are reported as null so the canvas treats them as background (no
58556
- * select/drag/context-menu/inline-edit).
58879
+ * See {@link resolveInteractiveElementId}.
58557
58880
  */
58558
58881
  interactiveElementIdAt(target) {
58559
- const host = target?.closest('[data-element-id]');
58560
- const id = host?.getAttribute('data-element-id');
58561
- if (!id) {
58562
- return null;
58563
- }
58564
- const el = this.allElements().find((e) => e.id === id);
58565
- if (!el) {
58566
- return null;
58567
- }
58568
- return isElementInteractive(el, true, this.editTemplateMode()) ? id : null;
58882
+ return resolveInteractiveElementId(target, this.allElements(), this.editTemplateMode());
58569
58883
  }
58570
58884
  onStagePointerDown(event) {
58571
58885
  if (!this.editable()) {
@@ -58582,43 +58896,8 @@ class SlideCanvasComponent {
58582
58896
  // ── DRAW BRANCH: must come before the select/marquee/drag path ─────────
58583
58897
  // When a draw tool is active, pointer gestures capture strokes; none of
58584
58898
  // the select/marquee/drag logic should run.
58585
- if (this.drawTool() !== 'select') {
58586
- const stage = this.stageRef()?.nativeElement;
58587
- if (!stage) {
58588
- return;
58589
- }
58590
- const rect = stage.getBoundingClientRect();
58591
- const zoom = this.effectiveScale() || 1;
58592
- const pt = {
58593
- x: (event.clientX - rect.left) / zoom,
58594
- y: (event.clientY - rect.top) / zoom,
58595
- };
58596
- if (this.drawTool() === 'eraser') {
58597
- // Find ink elements whose bounding box (+ 15px hit radius) contains
58598
- // the pointer. Iterate in reverse so the topmost element wins.
58599
- const HIT_RADIUS = 15;
58600
- const allElements = this.elements();
58601
- for (let i = allElements.length - 1; i >= 0; i--) {
58602
- const el = allElements[i];
58603
- if (el.type !== 'ink') {
58604
- continue;
58605
- }
58606
- if (pt.x >= el.x - HIT_RADIUS &&
58607
- pt.x <= el.x + el.width + HIT_RADIUS &&
58608
- pt.y >= el.y - HIT_RADIUS &&
58609
- pt.y <= el.y + el.height + HIT_RADIUS) {
58610
- this.eraserHit.emit(el.id);
58611
- break;
58612
- }
58613
- }
58614
- return;
58615
- }
58616
- // pen / highlighter / freeform: begin stroke
58617
- event.preventDefault();
58618
- event.target?.setPointerCapture?.(event.pointerId);
58619
- this.inkPoints = [pt];
58620
- this.inkActiveSignal.set(true);
58621
- this.liveInkPath.set(pointsToSvgPathD(this.inkPoints));
58899
+ if (this.inkDrawing.isDrawToolActive()) {
58900
+ this.inkDrawing.handleStagePointerDown(event);
58622
58901
  return;
58623
58902
  }
58624
58903
  // ── END DRAW BRANCH ─────────────────────────────────────────────────────
@@ -58828,36 +59107,13 @@ class SlideCanvasComponent {
58828
59107
  }
58829
59108
  onPointerMove(event) {
58830
59109
  // ── GUIDE DRAG ────────────────────────────────────────────────────────
58831
- if (this.guideDrag) {
58832
- const stage = this.stageRef()?.nativeElement;
58833
- if (stage) {
58834
- const rect = stage.getBoundingClientRect();
58835
- const z = this.effectiveScale() || 1;
58836
- const guides = this.rulerGuides();
58837
- const { id, axis } = this.guideDrag;
58838
- const rawPos = axis === 'x' ? (event.clientX - rect.left) / z : (event.clientY - rect.top) / z;
58839
- const clampMax = axis === 'x' ? this.canvasSize().width : this.canvasSize().height;
58840
- const pos = Math.max(0, Math.min(clampMax, rawPos));
58841
- this.rulerGuides.set(guides.map((g) => (g.id === id ? { ...g, pos } : g)));
58842
- }
59110
+ if (this.rulerGuidesSvc.handlePointerMove(event)) {
58843
59111
  return;
58844
59112
  }
58845
59113
  // ── END GUIDE DRAG ────────────────────────────────────────────────────
58846
59114
  // ── DRAW BRANCH ───────────────────────────────────────────────────────
58847
59115
  // When a stroke is in progress, consume all pointer-move events for drawing.
58848
- if (this.inkActiveSignal()) {
58849
- const stage = this.stageRef()?.nativeElement;
58850
- if (!stage) {
58851
- return;
58852
- }
58853
- const rect = stage.getBoundingClientRect();
58854
- const zoom = this.effectiveScale() || 1;
58855
- const pt = {
58856
- x: (event.clientX - rect.left) / zoom,
58857
- y: (event.clientY - rect.top) / zoom,
58858
- };
58859
- this.inkPoints.push(pt);
58860
- this.liveInkPath.set(pointsToSvgPathD(this.inkPoints));
59116
+ if (this.inkDrawing.handlePointerMove(event)) {
58861
59117
  return;
58862
59118
  }
58863
59119
  // ── END DRAW BRANCH ───────────────────────────────────────────────────
@@ -58941,7 +59197,7 @@ class SlideCanvasComponent {
58941
59197
  }
58942
59198
  // Guide snap: snap the moving element to the nearest user guide.
58943
59199
  if (this.snapToGuides()) {
58944
- const guides = this.rulerGuides();
59200
+ const guides = this.rulerGuidesSvc.rulerGuides();
58945
59201
  const thr = SNAP_SCREEN_PX / zoom;
58946
59202
  let gx = box.x;
58947
59203
  let gy = box.y;
@@ -58970,28 +59226,13 @@ class SlideCanvasComponent {
58970
59226
  }
58971
59227
  onPointerUp() {
58972
59228
  // ── GUIDE DRAG ────────────────────────────────────────────────────────
58973
- if (this.guideDrag) {
58974
- this.guideDrag = null;
59229
+ if (this.rulerGuidesSvc.handlePointerUp()) {
58975
59230
  return;
58976
59231
  }
58977
59232
  // ── END GUIDE DRAG ────────────────────────────────────────────────────
58978
59233
  // ── DRAW BRANCH ───────────────────────────────────────────────────────
58979
59234
  // Finalise the stroke and emit the completed ink element.
58980
- if (this.inkActiveSignal()) {
58981
- this.inkActiveSignal.set(false);
58982
- const tool = this.drawTool();
58983
- const resolvedTool = tool === 'highlighter' ? 'highlighter' : tool === 'freeform' ? 'freeform' : 'pen';
58984
- const ink = strokeToInkElement({
58985
- points: this.inkPoints,
58986
- color: this.drawColor(),
58987
- width: this.drawWidth(),
58988
- tool: resolvedTool,
58989
- });
58990
- if (ink) {
58991
- this.inkStrokeComplete.emit(ink);
58992
- }
58993
- this.inkPoints = [];
58994
- this.liveInkPath.set('');
59235
+ if (this.inkDrawing.handlePointerUp()) {
58995
59236
  return;
58996
59237
  }
58997
59238
  // ── END DRAW BRANCH ───────────────────────────────────────────────────
@@ -59014,59 +59255,6 @@ class SlideCanvasComponent {
59014
59255
  this.drag = null;
59015
59256
  this.snapGuides.set([]);
59016
59257
  }
59017
- /** Begin dragging an existing guide. Called from the guide handle pointerdown. */
59018
- onGuidePointerDown(event, id, axis) {
59019
- event.stopPropagation();
59020
- event.target?.setPointerCapture?.(event.pointerId);
59021
- this.guideDrag = { id, axis };
59022
- }
59023
- /** Remove a guide (called on guide handle double-click). */
59024
- onGuideDoubleClick(event, id) {
59025
- event.stopPropagation();
59026
- this.rulerGuides.update((gs) => gs.filter((g) => g.id !== id));
59027
- }
59028
- /** Drag from the horizontal ruler to create a new horizontal guide (axis:'y'). */
59029
- onHRulerPointerDown(event) {
59030
- if (!this.editable()) {
59031
- return;
59032
- }
59033
- const stage = this.stageRef()?.nativeElement;
59034
- if (!stage) {
59035
- return;
59036
- }
59037
- event.preventDefault();
59038
- event.target?.setPointerCapture?.(event.pointerId);
59039
- const rect = stage.getBoundingClientRect();
59040
- const z = this.effectiveScale() || 1;
59041
- const pos = (event.clientY - rect.top) / z;
59042
- const id = `guide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
59043
- this.rulerGuides.update((gs) => [
59044
- ...gs,
59045
- { id, axis: 'y', pos: Math.max(0, pos) },
59046
- ]);
59047
- this.guideDrag = { id, axis: 'y' };
59048
- }
59049
- /** Drag from the vertical ruler to create a new vertical guide (axis:'x'). */
59050
- onVRulerPointerDown(event) {
59051
- if (!this.editable()) {
59052
- return;
59053
- }
59054
- const stage = this.stageRef()?.nativeElement;
59055
- if (!stage) {
59056
- return;
59057
- }
59058
- event.preventDefault();
59059
- event.target?.setPointerCapture?.(event.pointerId);
59060
- const rect = stage.getBoundingClientRect();
59061
- const z = this.effectiveScale() || 1;
59062
- const pos = (event.clientX - rect.left) / z;
59063
- const id = `guide-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
59064
- this.rulerGuides.update((gs) => [
59065
- ...gs,
59066
- { id, axis: 'x', pos: Math.max(0, pos) },
59067
- ]);
59068
- this.guideDrag = { id, axis: 'x' };
59069
- }
59070
59258
  wrapperStyle = computed(() => {
59071
59259
  const scale = this.effectiveScale();
59072
59260
  const size = this.canvasSize();
@@ -59136,7 +59324,7 @@ class SlideCanvasComponent {
59136
59324
  }, /* @ts-ignore */
59137
59325
  ...(ngDevMode ? [{ debugName: "stageStyle" }] : /* istanbul ignore next */ []));
59138
59326
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: SlideCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
59139
- 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: `
59140
59328
  <div
59141
59329
  #viewport
59142
59330
  class="pptx-ng-canvas-viewport"
@@ -59353,7 +59541,7 @@ class SlideCanvasComponent {
59353
59541
  Each guide has a non-interactive line body and an interactive
59354
59542
  drag handle. Double-click the handle to delete the guide.
59355
59543
  -->
59356
- @for (g of rulerGuides(); track g.id) {
59544
+ @for (g of rulerGuidesSvc.rulerGuides(); track g.id) {
59357
59545
  <!-- Guide line body: pointer-events:none -->
59358
59546
  <div
59359
59547
  class="pptx-ng-ruler-guide-line"
@@ -59370,8 +59558,8 @@ class SlideCanvasComponent {
59370
59558
  [style.width]="g.axis === 'x' ? '9px' : '100%'"
59371
59559
  [style.height]="g.axis === 'y' ? '9px' : '100%'"
59372
59560
  [style.cursor]="g.axis === 'x' ? 'col-resize' : 'row-resize'"
59373
- (pointerdown)="onGuidePointerDown($event, g.id, g.axis)"
59374
- (dblclick)="onGuideDoubleClick($event, g.id)"
59561
+ (pointerdown)="rulerGuidesSvc.onGuidePointerDown($event, g.id, g.axis)"
59562
+ (dblclick)="rulerGuidesSvc.onGuideDoubleClick($event, g.id)"
59375
59563
  [title]="'pptx.canvas.guideTooltip' | translate"
59376
59564
  ></div>
59377
59565
  }
@@ -59382,7 +59570,7 @@ class SlideCanvasComponent {
59382
59570
  pointer-events:none so it never intercepts element gestures.
59383
59571
  No data-pptx-element / aria-roledescription / data-pptx-viewport.
59384
59572
  -->
59385
- @if (inkActiveSignal() && liveInkPath() && drawTool() !== 'select') {
59573
+ @if (inkDrawing.active() && inkDrawing.liveInkPath() && drawTool() !== 'select') {
59386
59574
  <svg
59387
59575
  class="pptx-ng-ink-preview"
59388
59576
  aria-hidden="true"
@@ -59391,7 +59579,7 @@ class SlideCanvasComponent {
59391
59579
  style="position:absolute;inset:0;pointer-events:none;z-index:70"
59392
59580
  >
59393
59581
  <path
59394
- [attr.d]="liveInkPath()"
59582
+ [attr.d]="inkDrawing.liveInkPath()"
59395
59583
  fill="none"
59396
59584
  [attr.stroke]="drawColor()"
59397
59585
  [attr.stroke-width]="drawWidth()"
@@ -59421,7 +59609,7 @@ class SlideCanvasComponent {
59421
59609
  [attr.width]="canvasSize().width * effectiveScalePublic()"
59422
59610
  [attr.height]="20"
59423
59611
  [style.cursor]="editable() ? 'crosshair' : null"
59424
- (pointerdown)="editable() ? onHRulerPointerDown($event) : null"
59612
+ (pointerdown)="editable() ? rulerGuidesSvc.onHRulerPointerDown($event) : null"
59425
59613
  >
59426
59614
  <rect
59427
59615
  [attr.width]="canvasSize().width * effectiveScalePublic()"
@@ -59466,7 +59654,7 @@ class SlideCanvasComponent {
59466
59654
  [attr.width]="20"
59467
59655
  [attr.height]="canvasSize().height * effectiveScalePublic()"
59468
59656
  [style.cursor]="editable() ? 'crosshair' : null"
59469
- (pointerdown)="editable() ? onVRulerPointerDown($event) : null"
59657
+ (pointerdown)="editable() ? rulerGuidesSvc.onVRulerPointerDown($event) : null"
59470
59658
  >
59471
59659
  <rect
59472
59660
  width="20"
@@ -59510,7 +59698,7 @@ class SlideCanvasComponent {
59510
59698
  }
59511
59699
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: SlideCanvasComponent, decorators: [{
59512
59700
  type: Component,
59513
- 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: `
59514
59702
  <div
59515
59703
  #viewport
59516
59704
  class="pptx-ng-canvas-viewport"
@@ -59727,7 +59915,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59727
59915
  Each guide has a non-interactive line body and an interactive
59728
59916
  drag handle. Double-click the handle to delete the guide.
59729
59917
  -->
59730
- @for (g of rulerGuides(); track g.id) {
59918
+ @for (g of rulerGuidesSvc.rulerGuides(); track g.id) {
59731
59919
  <!-- Guide line body: pointer-events:none -->
59732
59920
  <div
59733
59921
  class="pptx-ng-ruler-guide-line"
@@ -59744,8 +59932,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59744
59932
  [style.width]="g.axis === 'x' ? '9px' : '100%'"
59745
59933
  [style.height]="g.axis === 'y' ? '9px' : '100%'"
59746
59934
  [style.cursor]="g.axis === 'x' ? 'col-resize' : 'row-resize'"
59747
- (pointerdown)="onGuidePointerDown($event, g.id, g.axis)"
59748
- (dblclick)="onGuideDoubleClick($event, g.id)"
59935
+ (pointerdown)="rulerGuidesSvc.onGuidePointerDown($event, g.id, g.axis)"
59936
+ (dblclick)="rulerGuidesSvc.onGuideDoubleClick($event, g.id)"
59749
59937
  [title]="'pptx.canvas.guideTooltip' | translate"
59750
59938
  ></div>
59751
59939
  }
@@ -59756,7 +59944,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59756
59944
  pointer-events:none so it never intercepts element gestures.
59757
59945
  No data-pptx-element / aria-roledescription / data-pptx-viewport.
59758
59946
  -->
59759
- @if (inkActiveSignal() && liveInkPath() && drawTool() !== 'select') {
59947
+ @if (inkDrawing.active() && inkDrawing.liveInkPath() && drawTool() !== 'select') {
59760
59948
  <svg
59761
59949
  class="pptx-ng-ink-preview"
59762
59950
  aria-hidden="true"
@@ -59765,7 +59953,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59765
59953
  style="position:absolute;inset:0;pointer-events:none;z-index:70"
59766
59954
  >
59767
59955
  <path
59768
- [attr.d]="liveInkPath()"
59956
+ [attr.d]="inkDrawing.liveInkPath()"
59769
59957
  fill="none"
59770
59958
  [attr.stroke]="drawColor()"
59771
59959
  [attr.stroke-width]="drawWidth()"
@@ -59795,7 +59983,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59795
59983
  [attr.width]="canvasSize().width * effectiveScalePublic()"
59796
59984
  [attr.height]="20"
59797
59985
  [style.cursor]="editable() ? 'crosshair' : null"
59798
- (pointerdown)="editable() ? onHRulerPointerDown($event) : null"
59986
+ (pointerdown)="editable() ? rulerGuidesSvc.onHRulerPointerDown($event) : null"
59799
59987
  >
59800
59988
  <rect
59801
59989
  [attr.width]="canvasSize().width * effectiveScalePublic()"
@@ -59840,7 +60028,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
59840
60028
  [attr.width]="20"
59841
60029
  [attr.height]="canvasSize().height * effectiveScalePublic()"
59842
60030
  [style.cursor]="editable() ? 'crosshair' : null"
59843
- (pointerdown)="editable() ? onVRulerPointerDown($event) : null"
60031
+ (pointerdown)="editable() ? rulerGuidesSvc.onVRulerPointerDown($event) : null"
59844
60032
  >
59845
60033
  <rect
59846
60034
  width="20"
@@ -72590,170 +72778,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
72590
72778
  }]
72591
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"] }] } });
72592
72780
 
72593
- /**
72594
- * viewer-collaboration-session.service.ts: Viewer-scoped state + logic for the
72595
- * Share (two-way collaboration) and Broadcast (one-way owner) sessions. Owns the
72596
- * two dialog visibility flags and the active-session room/server, derives the
72597
- * shareable join / follow links, and drives connect / disconnect on the
72598
- * {@link CollaborationService}.
72599
- *
72600
- * Extracted from {@link PowerPointViewerComponent}: the component binds the host
72601
- * inputs it cannot reach from a service (author name, share defaults, the
72602
- * template-element snapshot supplier) plus the start/stop output emitters via
72603
- * {@link bind}; the template reads the flags / links / defaults and invokes the
72604
- * handlers off the injected instance.
72605
- *
72606
- * Provide it once on the viewer component
72607
- * (`providers: [ViewerCollaborationSessionService]`).
72608
- */
72609
- class ViewerCollaborationSessionService {
72610
- collab = inject(CollaborationService);
72611
- /** Share (collaboration) dialog visibility. */
72612
- showShare = signal(false, /* @ts-ignore */
72613
- ...(ngDevMode ? [{ debugName: "showShare" }] : /* istanbul ignore next */ []));
72614
- /** Broadcast dialog visibility. */
72615
- showBroadcast = signal(false, /* @ts-ignore */
72616
- ...(ngDevMode ? [{ debugName: "showBroadcast" }] : /* istanbul ignore next */ []));
72617
- /**
72618
- * Room/server of the currently active session, used to build the shareable
72619
- * join/follow links shown in the dialogs. Null when no session is active.
72620
- */
72621
- activeSession = signal(null, /* @ts-ignore */
72622
- ...(ngDevMode ? [{ debugName: "activeSession" }] : /* istanbul ignore next */ []));
72623
- host = null;
72624
- /** Wire the host inputs/outputs (called once from the component constructor). */
72625
- bind(host) {
72626
- this.host = host;
72627
- }
72628
- requireHost() {
72629
- if (!this.host) {
72630
- throw new Error('ViewerCollaborationSessionService.bind() was not called');
72631
- }
72632
- return this.host;
72633
- }
72634
- /** Browser location used to assemble share/follow URLs (omitted in SSR). */
72635
- browserLocation() {
72636
- return typeof window === 'undefined'
72637
- ? undefined
72638
- : { origin: window.location.origin, pathname: window.location.pathname };
72639
- }
72640
- /** Shareable join link for the active collaboration session. */
72641
- shareUrl = computed(() => {
72642
- const session = this.activeSession();
72643
- return session ? buildShareUrl(session.roomId, session.serverUrl, this.browserLocation()) : '';
72644
- }, /* @ts-ignore */
72645
- ...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
72646
- /** Shareable follow link for the active broadcast. */
72647
- broadcastViewerUrl = computed(() => {
72648
- const session = this.activeSession();
72649
- return session
72650
- ? buildBroadcastViewerUrl(session.roomId, session.serverUrl, this.browserLocation())
72651
- : '';
72652
- }, /* @ts-ignore */
72653
- ...(ngDevMode ? [{ debugName: "broadcastViewerUrl" }] : /* istanbul ignore next */ []));
72654
- /** Whether the active session is serverless (peer-to-peer / webrtc). */
72655
- activeSessionP2p = computed(() => (this.activeSession()?.serverUrl ?? '').trim().length === 0 && this.collab.active(), /* @ts-ignore */
72656
- ...(ngDevMode ? [{ debugName: "activeSessionP2p" }] : /* istanbul ignore next */ []));
72657
- /**
72658
- * Assemble the {@link ConnectOptions} shared by every connect call site: apply
72659
- * remote slides to the editor, size the presence bounds to the canvas, and
72660
- * expose the source bytes + separated template elements for write-back.
72661
- */
72662
- connectOptions() {
72663
- const host = this.requireHost();
72664
- const size = host.canvasSize();
72665
- return {
72666
- onRemoteSlides: (slides) => host.applyRemoteSlides(slides),
72667
- canvasWidth: size.width,
72668
- canvasHeight: size.height,
72669
- getSourceBytes: () => host.getSourceBytes(),
72670
- getTemplateElements: () => host.getTemplateElements(),
72671
- };
72672
- }
72673
- /**
72674
- * Connect and immediately seed the sync baseline with the current deck, so a
72675
- * joiner whose deck is still a placeholder never broadcasts (and overwrites)
72676
- * the shared document before the first remote sync arrives.
72677
- */
72678
- connectWithBaseline(config) {
72679
- void this.collab.connect(config, this.connectOptions());
72680
- this.collab.seedBaseline(this.requireHost().currentSlides());
72681
- }
72682
- /**
72683
- * Seed values for the Share dialog: the host-supplied `shareDefaults`, with
72684
- * `userName` falling back to `authorName` (then "You") so the local user's
72685
- * name pre-fills the form. Mirrors React/Vue.
72686
- */
72687
- shareDialogDefaults = computed(() => {
72688
- const host = this.host;
72689
- const defaults = host?.shareDefaults() ?? {};
72690
- return {
72691
- ...defaults,
72692
- userName: defaults.userName ?? host?.authorName() ?? 'You',
72693
- };
72694
- }, /* @ts-ignore */
72695
- ...(ngDevMode ? [{ debugName: "shareDialogDefaults" }] : /* istanbul ignore next */ []));
72696
- /**
72697
- * Connect / disconnect real-time collaboration when the host `collaboration`
72698
- * input changes (called from the component's effect).
72699
- */
72700
- syncHostConfig(config) {
72701
- if (config) {
72702
- this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
72703
- this.connectWithBaseline(config);
72704
- }
72705
- else {
72706
- this.collab.disconnect();
72707
- this.activeSession.set(null);
72708
- }
72709
- }
72710
- /** Start a real-time collaboration session from the share dialog config. */
72711
- onShareStart(config) {
72712
- const host = this.requireHost();
72713
- // Two-way collaboration: peers edit together (default `collaborator` role).
72714
- const collaboratorConfig = {
72715
- role: 'collaborator',
72716
- ...config,
72717
- userName: config.userName || (host.authorName() ?? 'You'),
72718
- };
72719
- this.activeSession.set({
72720
- roomId: collaboratorConfig.roomId,
72721
- serverUrl: collaboratorConfig.serverUrl,
72722
- });
72723
- this.connectWithBaseline(collaboratorConfig);
72724
- host.emitStart(collaboratorConfig);
72725
- }
72726
- onShareStop() {
72727
- this.collab.disconnect();
72728
- this.activeSession.set(null);
72729
- this.requireHost().emitStop();
72730
- }
72731
- /** Start broadcasting (presenter as session owner) from the broadcast config. */
72732
- onBroadcastStart(config) {
72733
- const host = this.requireHost();
72734
- const collabConfig = {
72735
- roomId: config.roomId,
72736
- serverUrl: config.serverUrl,
72737
- transport: config.transport,
72738
- userName: host.authorName() ?? 'Presenter',
72739
- role: 'owner',
72740
- };
72741
- this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
72742
- this.connectWithBaseline(collabConfig);
72743
- host.emitStart(collabConfig);
72744
- }
72745
- onBroadcastStop() {
72746
- this.collab.disconnect();
72747
- this.activeSession.set(null);
72748
- this.requireHost().emitStop();
72749
- }
72750
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
72751
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService });
72752
- }
72753
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerCollaborationSessionService, decorators: [{
72754
- type: Injectable
72755
- }] });
72756
-
72757
72781
  /**
72758
72782
  * viewer-dialogs.service.ts: Viewer-scoped open-state + light state for the
72759
72783
  * secondary dialogs and side panels (equation editor, set-up-slide-show,
@@ -72856,6 +72880,694 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
72856
72880
  type: Injectable
72857
72881
  }] });
72858
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
+
72859
73571
  /**
72860
73572
  * viewer-extra-dialogs-helpers.ts: Pure helpers backing
72861
73573
  * {@link ViewerExtraDialogsComponent}. Kept framework-free (no Angular / DOM)
@@ -73137,6 +73849,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
73137
73849
  type: Injectable
73138
73850
  }] });
73139
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
+
73140
73924
  /**
73141
73925
  * viewer-export.service.ts: Viewer-scoped orchestration for the "render every
73142
73926
  * slide to the live stage and capture it" family of actions: single-slide PNG,
@@ -76501,6 +77285,104 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
76501
77285
  }]
76502
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"] }] } });
76503
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
+
76504
77386
  /**
76505
77387
  * viewer-find-replace.service.ts: Viewer-scoped state + logic for the
76506
77388
  * find-in-slides bar and the edit-mode find-and-replace bar. Owns the two bar
@@ -76592,264 +77474,177 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
76592
77474
  }] });
76593
77475
 
76594
77476
  /**
76595
- * 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.
76596
77481
  *
76597
- * Prefers the native browser EyeDropper API (Chrome 95+ / Edge 95+). For
76598
- * browsers without it (Firefox, Safari) a DOM-sampling fallback reads the
76599
- * colour under the pointer via `elementFromPoint` + `getComputedStyle`, so the
76600
- * eyedropper still works everywhere. Mirrors the React viewer's
76601
- * `viewer/utils/eyedropper.ts`.
76602
- */
76603
- /**
76604
- * Returns true when the native EyeDropper API is available in this browser.
76605
- * Guards against SSR / headless environments without `window`.
76606
- */
76607
- function eyedropperAvailable() {
76608
- return typeof window !== 'undefined' && 'EyeDropper' in window;
76609
- }
76610
- /**
76611
- * Open the native EyeDropper and return the picked sRGB hex colour string
76612
- * (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).
76613
77486
  */
76614
- async function openNativeEyeDropper() {
76615
- if (!eyedropperAvailable()) {
76616
- return null;
76617
- }
76618
- try {
76619
- const EyeDropperClass = window['EyeDropper'];
76620
- const dropper = new EyeDropperClass();
76621
- const result = await dropper.open();
76622
- return result.sRGBHex;
76623
- }
76624
- catch {
76625
- // User cancelled (AbortError) or unexpected error: treat as no-op
76626
- return null;
76627
- }
76628
- }
76629
- // ---------------------------------------------------------------------------
76630
- // Fallback sampling (Firefox / Safari)
76631
- // ---------------------------------------------------------------------------
76632
- function toHex(r, g, b) {
76633
- const h = (n) => n.toString(16).padStart(2, '0');
76634
- return `#${h(r)}${h(g)}${h(b)}`;
76635
- }
76636
- function parseRgbaString(str) {
76637
- const match = str.match(/rgba?\(\s*(?<r>\d+)\s*,\s*(?<g>\d+)\s*,\s*(?<b>\d+)/u);
76638
- if (!match?.groups) {
76639
- return null;
76640
- }
76641
- const r = parseInt(match.groups.r, 10);
76642
- const g = parseInt(match.groups.g, 10);
76643
- const b = parseInt(match.groups.b, 10);
76644
- return { r, g, b, hex: toHex(r, g, b) };
76645
- }
77487
+ /** Downward drag distance (px) past which releasing the pointer dismisses. Matches `pptx-mobile-sheet`'s DISMISS_THRESHOLD. */
77488
+ const DISMISS_THRESHOLD_PX = 120;
76646
77489
  /**
76647
- * Sample the colour at a client-space point by inspecting the topmost element's
76648
- * computed paint. Tries, in order: a real `<canvas>` pixel read (sharp for
76649
- * rasterised content), then the element's `background-color`, SVG `fill`, and
76650
- * finally text `color`. Returns `null` when nothing paintable is found.
76651
- *
76652
- * This is the DOM fallback used when the native EyeDropper API is unavailable;
76653
- * 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}.
76654
77492
  */
76655
- function sampleColorFromSlide(clientX, clientY) {
76656
- const target = typeof document === 'undefined' ? null : document.elementFromPoint(clientX, clientY);
76657
- if (!(target instanceof Element)) {
76658
- return null;
76659
- }
76660
- // Direct pixel read when the pointer is over a <canvas> (untainted only).
76661
- const canvas = target instanceof HTMLCanvasElement ? target : target.closest('canvas');
76662
- if (canvas instanceof HTMLCanvasElement) {
76663
- try {
76664
- const ctx = canvas.getContext('2d');
76665
- if (ctx) {
76666
- const rect = canvas.getBoundingClientRect();
76667
- const sx = Math.round((clientX - rect.left) * (canvas.width / canvas.clientWidth));
76668
- const sy = Math.round((clientY - rect.top) * (canvas.height / canvas.clientHeight));
76669
- const pixel = ctx.getImageData(sx, sy, 1, 1).data;
76670
- 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;
76671
77510
  }
76672
- }
76673
- catch {
76674
- // Cross-origin / tainted canvas: fall through to computed-style sampling.
76675
- }
76676
- }
76677
- const computed = getComputedStyle(target);
76678
- const bg = computed.backgroundColor;
76679
- if (bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') {
76680
- const parsed = parseRgbaString(bg);
76681
- if (parsed) {
76682
- return parsed;
76683
- }
76684
- }
76685
- const fill = computed.fill;
76686
- if (fill && fill !== 'none' && fill !== 'transparent') {
76687
- const parsed = parseRgbaString(fill);
76688
- if (parsed) {
76689
- return parsed;
76690
- }
76691
- }
76692
- return computed.color ? parseRgbaString(computed.color) : null;
76693
- }
76694
- /**
76695
- * Run the eyedropper fallback: arm a one-shot pointer listener and resolve with
76696
- * the hex colour of the next click (or `null` if the user presses Escape). Used
76697
- * only when {@link eyedropperAvailable} is false. The caller is responsible for
76698
- * any "armed" UI affordance; this just manages the listeners.
76699
- */
76700
- function pickColorByClickFallback() {
76701
- if (typeof document === 'undefined') {
76702
- return Promise.resolve(null);
76703
- }
76704
- return new Promise((resolve) => {
76705
- const cleanup = () => {
76706
- document.removeEventListener('pointerdown', onPointerDown, true);
76707
- document.removeEventListener('keydown', onKeyDown, true);
76708
- };
76709
- const onPointerDown = (event) => {
76710
- event.preventDefault();
76711
- event.stopPropagation();
76712
- cleanup();
76713
- const sample = sampleColorFromSlide(event.clientX, event.clientY);
76714
- resolve(sample ? sample.hex : null);
76715
- };
76716
- const onKeyDown = (event) => {
76717
- if (event.key === 'Escape') {
76718
- cleanup();
76719
- resolve(null);
77511
+ dragYSignal.set(Math.max(0, event.clientY - startY));
77512
+ },
77513
+ onPointerUp(event) {
77514
+ if (startY === null) {
77515
+ return;
76720
77516
  }
76721
- };
76722
- document.addEventListener('pointerdown', onPointerDown, true);
76723
- document.addEventListener('keydown', onKeyDown, true);
76724
- });
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
+ };
76725
77527
  }
76726
77528
 
76727
77529
  /**
76728
- * Thin re-export shim vendored `pptx-viewer-shared`.
76729
- *
76730
- * The format-painter copy/apply logic was consolidated into
76731
- * `pptx-viewer-shared` (`render/format-painter.ts`), shared by every binding.
76732
- * This shim keeps the historical Angular import path for the component and its
76733
- * tests.
76734
- */
76735
-
76736
- /**
76737
- * viewer-format-painter.service.ts: Viewer-scoped state + logic for the format
76738
- * painter (copy one element's shape/text style onto the next clicked element)
76739
- * and the eyedropper (sample a screen colour onto the selected shape's fill,
76740
- * else copy it to the clipboard). Both are "apply a style to an element"
76741
- * 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.
76742
77536
  *
76743
77537
  * Extracted from {@link PowerPointViewerComponent}: the component binds the
76744
- * selection / active-slide accessors via {@link bind}; the template reads the
76745
- * armed/active flags and invokes the toggles, and the component's selection and
76746
- * keyboard handlers consult {@link active} / call {@link applyToTarget} /
76747
- * {@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`).
76748
77541
  *
76749
- * Provide it once on the viewer component (`providers: [ViewerFormatPainterService]`).
77542
+ * Provide it once on the viewer component (`providers: [ViewerInspectorPanelService]`).
76750
77543
  */
76751
- class ViewerFormatPainterService {
76752
- editor = inject(EditorStateService);
76753
- /** True while the painter is armed (next element click applies the copied format). */
76754
- active = signal(false, /* @ts-ignore */
76755
- ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
76756
- /** Whether the eyedropper is currently active. */
76757
- eyedropperActive = signal(false, /* @ts-ignore */
76758
- ...(ngDevMode ? [{ debugName: "eyedropperActive" }] : /* istanbul ignore next */ []));
76759
- /** Format copied from the source element when the painter was armed. */
76760
- 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
+ });
76761
77562
  host = null;
76762
- /** Wire the host selection/slide accessors (called once from the constructor). */
77563
+ /** Wire the host accessors (called once from the component constructor). */
76763
77564
  bind(host) {
76764
77565
  this.host = host;
76765
77566
  }
76766
77567
  requireHost() {
76767
77568
  if (!this.host) {
76768
- throw new Error('ViewerFormatPainterService.bind() was not called');
77569
+ throw new Error('ViewerInspectorPanelService.bind() was not called');
76769
77570
  }
76770
77571
  return this.host;
76771
77572
  }
76772
- /** Whether the painter can be armed: exactly one selected element with copyable format. */
76773
- canActivate = computed(() => hasCopyableFormat(this.host?.selectedElement() ?? null), /* @ts-ignore */
76774
- ...(ngDevMode ? [{ debugName: "canActivate" }] : /* istanbul ignore next */ []));
76775
- /** Toggle the format painter: arm from the current selection, or disarm. */
76776
- toggle() {
76777
- if (this.active()) {
76778
- this.cancel();
76779
- return;
76780
- }
76781
- const source = this.requireHost().selectedElement();
76782
- if (!source || !hasCopyableFormat(source)) {
76783
- return;
76784
- }
76785
- this.copiedFormat = copyFormatFromElement(source);
76786
- this.active.set(true);
76787
- }
76788
- /** Disarm the painter and drop the copied format. */
76789
- cancel() {
76790
- this.active.set(false);
76791
- this.copiedFormat = null;
76792
- }
76793
- /** Apply the copied format to a target element (shape/text style only; one history entry). */
76794
- applyToTarget(id) {
76795
- 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(() => {
76796
77580
  const host = this.requireHost();
76797
- const target = host.findActiveElement(id);
76798
- if (!format || !target) {
76799
- return;
77581
+ const panel = this.activePanel();
77582
+ if (panel === 'accessibility') {
77583
+ return 'accessibility';
76800
77584
  }
76801
- const updated = applyFormatToElement(target, format);
76802
- const patch = {};
76803
- if (format.shapeStyle && updated['shapeStyle'] !== undefined) {
76804
- patch['shapeStyle'] = updated['shapeStyle'];
77585
+ if (panel === 'signatures') {
77586
+ return 'signatures';
76805
77587
  }
76806
- if (format.textStyle && updated['textStyle'] !== undefined) {
76807
- patch['textStyle'] = updated['textStyle'];
77588
+ if (!host.canEdit()) {
77589
+ return null;
76808
77590
  }
76809
- if (Object.keys(patch).length > 0) {
76810
- this.editor.updateElement(host.activeSlideIndex(), id, patch);
77591
+ if (panel === 'comments') {
77592
+ return 'comments';
76811
77593
  }
76812
- }
76813
- /**
76814
- * Activate the eyedropper to pick a colour from the screen. Uses the native
76815
- * EyeDropper API where available (Chrome/Edge); on Firefox/Safari it falls
76816
- * back to a one-shot click that samples the slide DOM under the pointer.
76817
- * When a shape/text/connector/image element is selected, applies the colour
76818
- * to its fill; otherwise copies it to the clipboard. No-ops when the user
76819
- * cancels (Escape) or nothing paintable is under the pointer.
76820
- */
76821
- async toggleEyedropper() {
76822
- this.eyedropperActive.set(true);
76823
- try {
76824
- const color = eyedropperAvailable()
76825
- ? await openNativeEyeDropper()
76826
- : await pickColorByClickFallback();
76827
- if (color) {
76828
- await this.applyEyedropperColor(color);
76829
- }
77594
+ if (panel === 'selection') {
77595
+ return 'selection';
76830
77596
  }
76831
- finally {
76832
- this.eyedropperActive.set(false);
77597
+ if (host.selectedElement()) {
77598
+ return 'element';
76833
77599
  }
76834
- }
76835
- /** Apply a picked colour to the selected shape's fill, else copy to clipboard. */
76836
- async applyEyedropperColor(color) {
76837
- const host = this.requireHost();
76838
- const sel = host.selectedElement();
76839
- const idx = host.activeSlideIndex();
76840
- if (sel !== null && hasShapeProperties(sel)) {
76841
- this.editor.updateElement(idx, sel.id, {
76842
- shapeStyle: { ...sel.shapeStyle, fillColor: color },
76843
- });
77600
+ if (host.activeSlide()) {
77601
+ return 'slide';
76844
77602
  }
76845
- else {
76846
- 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 '';
76847
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);
76848
77643
  }
76849
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerFormatPainterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
76850
- 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 });
76851
77646
  }
76852
- 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: [{
76853
77648
  type: Injectable
76854
77649
  }] });
76855
77650
 
@@ -76980,16 +77775,339 @@ class ViewerKeyboardService {
76980
77775
  break;
76981
77776
  }
76982
77777
  }
76983
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerKeyboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
76984
- 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 });
76985
78106
  }
76986
- 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: [{
76987
78108
  type: Injectable
76988
78109
  }] });
76989
78110
 
76990
- const ZOOM_STEP = 0.1;
76991
- const ZOOM_MIN = 0.2;
76992
- const ZOOM_MAX = 3;
76993
78111
  /**
76994
78112
  * PowerPointViewerComponent: Angular port of the React `PowerPointViewer.tsx`
76995
78113
  * and Vue `PowerPointViewer.vue`.
@@ -77077,7 +78195,6 @@ class PowerPointViewerComponent {
77077
78195
  /** Fired when the collaboration/broadcast session stops. Mirrors React/Vue. */
77078
78196
  stopCollaboration = output();
77079
78197
  loader = inject(LoadContentService);
77080
- exportSvc = inject(ExportService);
77081
78198
  editor = inject(EditorStateService);
77082
78199
  fonts = inject(EmbeddedFontsService);
77083
78200
  collab = inject(CollaborationService);
@@ -77095,6 +78212,16 @@ class PowerPointViewerComponent {
77095
78212
  session = inject(ViewerCollaborationSessionService);
77096
78213
  formatPainter = inject(ViewerFormatPainterService);
77097
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);
77098
78225
  /** Handle on the secondary-dialog host (keep-annotations prompt). */
77099
78226
  extraDialogs = viewChild(ViewerExtraDialogsComponent, /* @ts-ignore */
77100
78227
  ...(ngDevMode ? [{ debugName: "extraDialogs" }] : /* istanbul ignore next */ []));
@@ -77141,36 +78268,9 @@ class PowerPointViewerComponent {
77141
78268
  ...(ngDevMode ? [{ debugName: "activeTemplateElements" }] : /* istanbul ignore next */ []));
77142
78269
  rootStyle = computed(() => themeStyle(this.theme()), /* @ts-ignore */
77143
78270
  ...(ngDevMode ? [{ debugName: "rootStyle" }] : /* istanbul ignore next */ []));
77144
- zoom = signal(1, /* @ts-ignore */
77145
- ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
77146
- zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
77147
- ...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
77148
- /**
77149
- * Remote cursors filtered to the slide the local user is viewing, so peers'
77150
- * cursors only appear on the shared slide (mirrors React/Vue).
77151
- */
77152
- collabCursors = computed(() => presenceToCursors(this.collab.presence(), this.activeSlideIndex()), /* @ts-ignore */
77153
- ...(ngDevMode ? [{ debugName: "collabCursors" }] : /* istanbul ignore next */ []));
77154
- /** Timestamp of the last cursor broadcast (throttle gate). */
77155
- lastCursorBroadcast = 0;
77156
- /** Fullscreen presentation-mode overlay visibility. */
77157
- presenting = signal(false, /* @ts-ignore */
77158
- ...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
77159
- /** Presenter-view (speaker) overlay visibility. */
77160
- presentingPresenter = signal(false, /* @ts-ignore */
77161
- ...(ngDevMode ? [{ debugName: "presentingPresenter" }] : /* istanbul ignore next */ []));
77162
- /** Epoch ms when presenter view started (drives the elapsed timer). */
77163
- presenterStartTime = signal(null, /* @ts-ignore */
77164
- ...(ngDevMode ? [{ debugName: "presenterStartTime" }] : /* istanbul ignore next */ []));
77165
78271
  /** Slide-sorter grid overlay visibility. */
77166
78272
  showSorter = signal(false, /* @ts-ignore */
77167
78273
  ...(ngDevMode ? [{ debugName: "showSorter" }] : /* istanbul ignore next */ []));
77168
- /** Open mobile bottom-sheet (slides / menu), or null. */
77169
- mobileSheet = signal(null, /* @ts-ignore */
77170
- ...(ngDevMode ? [{ debugName: "mobileSheet" }] : /* istanbul ignore next */ []));
77171
- /** Speaker-notes strip visibility. */
77172
- showNotes = signal(false, /* @ts-ignore */
77173
- ...(ngDevMode ? [{ debugName: "showNotes" }] : /* istanbul ignore next */ []));
77174
78274
  /** Whether the left slides panel is collapsed (top-bar sidebar toggle). */
77175
78275
  slidesPanelCollapsed = signal(false, /* @ts-ignore */
77176
78276
  ...(ngDevMode ? [{ debugName: "slidesPanelCollapsed" }] : /* istanbul ignore next */ []));
@@ -77184,73 +78284,6 @@ class PowerPointViewerComponent {
77184
78284
  /** Active ink stroke width in stage pixels. */
77185
78285
  activeDrawWidth = signal(3, /* @ts-ignore */
77186
78286
  ...(ngDevMode ? [{ debugName: "activeDrawWidth" }] : /* istanbul ignore next */ []));
77187
- /** Active right-docked tool panel (comments / accessibility / selection), or null. */
77188
- activePanel = signal(null, /* @ts-ignore */
77189
- ...(ngDevMode ? [{ debugName: "activePanel" }] : /* istanbul ignore next */ []));
77190
- /**
77191
- * Which panel the single inspector host should show, applying the original
77192
- * first-match precedence (explicit tool panels → element → slide default).
77193
- * `accessibility`/`signatures` render regardless of edit mode; the rest need
77194
- * `canEdit`.
77195
- */
77196
- inspectorContent = computed(() => {
77197
- const panel = this.activePanel();
77198
- if (panel === 'accessibility') {
77199
- return 'accessibility';
77200
- }
77201
- if (panel === 'signatures') {
77202
- return 'signatures';
77203
- }
77204
- if (!this.canEdit()) {
77205
- return null;
77206
- }
77207
- if (panel === 'comments') {
77208
- return 'comments';
77209
- }
77210
- if (panel === 'selection') {
77211
- return 'selection';
77212
- }
77213
- if (this.selectedElement()) {
77214
- return 'element';
77215
- }
77216
- if (this.activeSlide()) {
77217
- return 'slide';
77218
- }
77219
- return null;
77220
- }, /* @ts-ignore */
77221
- ...(ngDevMode ? [{ debugName: "inspectorContent" }] : /* istanbul ignore next */ []));
77222
- /**
77223
- * Whether the right-docked inspector is showing the format panel (element or
77224
- * slide properties). Drives the top-bar inspector-toggle active state.
77225
- */
77226
- inspectorPaneOpen = computed(() => {
77227
- const content = this.inspectorContent();
77228
- return content === 'element' || content === 'slide';
77229
- }, /* @ts-ignore */
77230
- ...(ngDevMode ? [{ debugName: "inspectorPaneOpen" }] : /* istanbul ignore next */ []));
77231
- /** Inspector content, but null on mobile once the user has swiped it away. */
77232
- visibleInspectorKind = computed(() => this.mobile.isMobile() && this.mobileInspectorHidden() ? null : this.inspectorContent(), /* @ts-ignore */
77233
- ...(ngDevMode ? [{ debugName: "visibleInspectorKind" }] : /* istanbul ignore next */ []));
77234
- /** Accessible-label translation key for the inspector host, by active content. */
77235
- inspectorLabel = computed(() => {
77236
- switch (this.inspectorContent()) {
77237
- case 'accessibility':
77238
- return 'pptx.accessibility.title';
77239
- case 'signatures':
77240
- return 'pptx.viewer.digitalSignatures';
77241
- case 'comments':
77242
- return 'pptx.toolbar.comments';
77243
- case 'selection':
77244
- return 'pptx.selectionPane.title';
77245
- case 'element':
77246
- return 'pptx.viewer.elementProperties';
77247
- case 'slide':
77248
- return 'pptx.viewer.slideProperties';
77249
- default:
77250
- return '';
77251
- }
77252
- }, /* @ts-ignore */
77253
- ...(ngDevMode ? [{ debugName: "inspectorLabel" }] : /* istanbul ignore next */ []));
77254
78287
  /**
77255
78288
  * Which mobile bottom-bar slot is currently "active" (highlighted). The
77256
78289
  * comments panel maps to the Comments slot; an open notes strip maps to
@@ -77258,13 +78291,13 @@ class PowerPointViewerComponent {
77258
78291
  * selected the inspector (Format) is showing inline so it maps to inspector.
77259
78292
  */
77260
78293
  mobileBarSheet = computed(() => {
77261
- if (this.mobileSheet() === 'slides') {
78294
+ if (this.mobileSheetSvc.mobileSheet() === 'slides') {
77262
78295
  return 'slides';
77263
78296
  }
77264
- if (this.activePanel() === 'comments') {
78297
+ if (this.inspectorPanel.activePanel() === 'comments') {
77265
78298
  return 'comments';
77266
78299
  }
77267
- if (this.showNotes()) {
78300
+ if (this.mobileSheetSvc.showNotes()) {
77268
78301
  return 'notes';
77269
78302
  }
77270
78303
  if (this.selectedElement()) {
@@ -77273,30 +78306,9 @@ class PowerPointViewerComponent {
77273
78306
  return null;
77274
78307
  }, /* @ts-ignore */
77275
78308
  ...(ngDevMode ? [{ debugName: "mobileBarSheet" }] : /* istanbul ignore next */ []));
77276
- /** Document-properties (Info) dialog visibility. */
77277
- showProperties = signal(false, /* @ts-ignore */
77278
- ...(ngDevMode ? [{ debugName: "showProperties" }] : /* istanbul ignore next */ []));
77279
- /** Hyperlink-edit dialog visibility. */
77280
- showHyperlink = signal(false, /* @ts-ignore */
77281
- ...(ngDevMode ? [{ debugName: "showHyperlink" }] : /* istanbul ignore next */ []));
77282
- /** Local overrides applied to document properties via the Info dialog. */
77283
- coreOverride = signal({}, /* @ts-ignore */
77284
- ...(ngDevMode ? [{ debugName: "coreOverride" }] : /* istanbul ignore next */ []));
77285
78309
  /** Comments on the active slide. */
77286
78310
  activeComments = computed(() => this.activeSlide()?.comments ?? [], /* @ts-ignore */
77287
78311
  ...(ngDevMode ? [{ debugName: "activeComments" }] : /* istanbul ignore next */ []));
77288
- /** Document core properties (loaded, with any in-session edits merged in). */
77289
- coreProperties = computed(() => ({
77290
- ...(this.loader.coreProperties() ?? {}),
77291
- ...this.coreOverride(),
77292
- }), /* @ts-ignore */
77293
- ...(ngDevMode ? [{ debugName: "coreProperties" }] : /* istanbul ignore next */ []));
77294
- /** Open editor context-menu position (client coords), or null. */
77295
- contextMenuPos = signal(null, /* @ts-ignore */
77296
- ...(ngDevMode ? [{ debugName: "contextMenuPos" }] : /* istanbul ignore next */ []));
77297
- /** Id of the element being inline text-edited, or null. */
77298
- editingId = signal(null, /* @ts-ignore */
77299
- ...(ngDevMode ? [{ debugName: "editingId" }] : /* istanbul ignore next */ []));
77300
78312
  /** Whether the dot-grid overlay is visible on the editor canvas. */
77301
78313
  showGrid = signal(false, /* @ts-ignore */
77302
78314
  ...(ngDevMode ? [{ debugName: "showGrid" }] : /* istanbul ignore next */ []));
@@ -77309,15 +78321,9 @@ class PowerPointViewerComponent {
77309
78321
  /** Whether snap-to-grid is active on the editor canvas. */
77310
78322
  snapToGrid = signal(false, /* @ts-ignore */
77311
78323
  ...(ngDevMode ? [{ debugName: "snapToGrid" }] : /* istanbul ignore next */ []));
77312
- /** Whether the theme-gallery overlay is visible (Design → Browse Themes). */
77313
- showThemeGallery = signal(false, /* @ts-ignore */
77314
- ...(ngDevMode ? [{ debugName: "showThemeGallery" }] : /* istanbul ignore next */ []));
77315
78324
  /** Whether the Insert SmartArt gallery dialog is open. */
77316
78325
  showSmartArtInsert = signal(false, /* @ts-ignore */
77317
78326
  ...(ngDevMode ? [{ debugName: "showSmartArtInsert" }] : /* istanbul ignore next */ []));
77318
- /** The `name` property of the loaded deck's theme (for check-mark in gallery). */
77319
- activeThemeName = computed(() => this.loader.theme()?.name, /* @ts-ignore */
77320
- ...(ngDevMode ? [{ debugName: "activeThemeName" }] : /* istanbul ignore next */ []));
77321
78327
  /**
77322
78328
  * Stable, always-truthy key for the slide-properties form. Changes only when
77323
78329
  * the active slide changes, so the `@if` recreates (and reseeds) the
@@ -77340,13 +78346,6 @@ class PowerPointViewerComponent {
77340
78346
  null);
77341
78347
  }, /* @ts-ignore */
77342
78348
  ...(ngDevMode ? [{ debugName: "selectedElement" }] : /* istanbul ignore next */ []));
77343
- /**
77344
- * Built-in File ▸ Open override of the `content` input. The native picker
77345
- * sets this to swap the deck in place; a fresh `content` input clears it so
77346
- * external reloads always win.
77347
- */
77348
- contentOverride = signal(null, /* @ts-ignore */
77349
- ...(ngDevMode ? [{ debugName: "contentOverride" }] : /* istanbul ignore next */ []));
77350
78349
  constructor() {
77351
78350
  // Surface the `smartArt3D` opt-in to the element dispatcher via the
77352
78351
  // viewer-scoped SmartArt3DService.
@@ -77356,12 +78355,11 @@ class PowerPointViewerComponent {
77356
78355
  // A new host `content` input supersedes any in-place picked file.
77357
78356
  effect(() => {
77358
78357
  this.content();
77359
- this.contentOverride.set(null);
78358
+ this.fileIO.contentOverride.set(null);
77360
78359
  });
77361
78360
  // Load whenever the active content (picked override, else input) changes.
77362
78361
  effect(() => {
77363
- const content = this.contentOverride() ?? this.content();
77364
- void this.loader.load(content);
78362
+ void this.loader.load(this.fileIO.activeContent());
77365
78363
  });
77366
78364
  // Reset to the first slide and seed the editable deck whenever a new
77367
78365
  // presentation finishes loading.
@@ -77374,7 +78372,7 @@ class PowerPointViewerComponent {
77374
78372
  // it on mobile — tapping a shape to edit it should surface its properties.
77375
78373
  effect(() => {
77376
78374
  if (this.selectedElement()) {
77377
- this.mobileInspectorHidden.set(false);
78375
+ this.inspectorPanel.mobileInspectorHidden.set(false);
77378
78376
  }
77379
78377
  });
77380
78378
  // Emit navigation changes.
@@ -77481,7 +78479,7 @@ class PowerPointViewerComponent {
77481
78479
  getTemplateElements: () => this.editor.templateElementsBySlideId(),
77482
78480
  applyRemoteSlides: (slides) => this.editor.applyRemoteSlides(slides),
77483
78481
  canvasSize: () => this.loader.canvasSize(),
77484
- getSourceBytes: () => this.currentSourceBytes(),
78482
+ getSourceBytes: () => this.fileIO.sourceBytes(),
77485
78483
  currentSlides: () => this.editor.slides(),
77486
78484
  emitStart: (config) => this.startCollaboration.emit(config),
77487
78485
  emitStop: () => this.stopCollaboration.emit(),
@@ -77497,34 +78495,88 @@ class PowerPointViewerComponent {
77497
78495
  // on (the @HostListener stays on the component).
77498
78496
  this.keyboard.bind({
77499
78497
  canEdit: () => this.canEdit(),
77500
- presenting: () => this.presenting(),
78498
+ presenting: () => this.presentationMode.presenting(),
77501
78499
  activeSlideIndex: () => this.activeSlideIndex(),
77502
78500
  });
77503
78501
  // Attach multi-touch gestures (pinch-zoom / swipe-nav / long-press menu)
77504
- // to the canvas host once it is rendered. See setupTouchGestures().
77505
- 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
+ });
77506
78573
  }
77507
78574
  /**
77508
78575
  * Serialise the current presentation to `.pptx` bytes (imperative handle).
77509
78576
  * When editing, this serialises the editor's edited deck so changes persist.
77510
78577
  */
77511
78578
  async getContent() {
77512
- const data = this.canEdit()
77513
- ? await this.loader.saveSlides(buildSaveSlides(this.editor.slides(), this.editor.templateElementsBySlideId()))
77514
- : await this.loader.getContent();
77515
- // Mirror React's imperative handle: serialising the deck also notifies the
77516
- // host so listeners wired to (contentChange) receive the latest bytes.
77517
- this.contentChange.emit(data);
77518
- return data;
77519
- }
77520
- /**
77521
- * Serialise the current deck and trigger a browser download of the `.pptx`.
77522
- * Surfaced on the mobile toolbar so saving is reachable without the desktop
77523
- * ribbon's File tab.
77524
- */
77525
- async saveAsPptx() {
77526
- const bytes = await this.getContent();
77527
- this.exportSvc.savePptx(bytes, 'presentation.pptx');
78579
+ return this.fileIO.getContent();
77528
78580
  }
77529
78581
  goTo(index) {
77530
78582
  if (index < 0 || index >= this.slideCount()) {
@@ -77538,318 +78590,13 @@ class PowerPointViewerComponent {
77538
78590
  goNext() {
77539
78591
  this.goTo(this.activeSlideIndex() + 1);
77540
78592
  }
77541
- /**
77542
- * Publish the local cursor while the pointer moves over the canvas. Throttled
77543
- * to {@link BROADCAST_THROTTLE_MS}; coordinates are mapped from client space
77544
- * into unscaled slide space (dividing by zoom, matching the cursor overlay)
77545
- * and clamped to the canvas bounds.
77546
- */
77547
- onCollabPointerMove(event) {
77548
- if (!this.collab.active()) {
77549
- return;
77550
- }
77551
- const now = Date.now();
77552
- if (now - this.lastCursorBroadcast < BROADCAST_THROTTLE_MS) {
77553
- return;
77554
- }
77555
- this.lastCursorBroadcast = now;
77556
- const host = this.mainEl()?.nativeElement;
77557
- if (!host) {
77558
- return;
77559
- }
77560
- const rect = host.getBoundingClientRect();
77561
- const zoom = this.zoom() || 1;
77562
- const size = this.loader.canvasSize();
77563
- const x = clampCursorPosition((event.clientX - rect.left) / zoom, 0, size.width);
77564
- const y = clampCursorPosition((event.clientY - rect.top) / zoom, 0, size.height);
77565
- this.collab.setCursor(x, y, this.activeSlideIndex());
77566
- }
77567
- /** The loaded source `.pptx` bytes (for elected-writer write-back), if any. */
77568
- currentSourceBytes() {
77569
- const content = this.contentOverride() ?? this.content();
77570
- if (!content) {
77571
- return null;
77572
- }
77573
- return content instanceof Uint8Array ? content : new Uint8Array(content);
77574
- }
77575
- // ── Theme gallery (Design tab) ─────────────────────────────────────────────
77576
- /**
77577
- * Apply a built-in theme preset to the whole deck.
77578
- *
77579
- * Mirrors Vue's `applyThemePreset()`: re-resolves slide colours via core's
77580
- * pure `applyThemeToData`, then writes the updated slides + theme metadata
77581
- * into `EditorStateService` as a single undoable entry. Also refreshes the
77582
- * `loader.themeColorMap` so subsequent theme switches start from the correct
77583
- * baseline.
77584
- */
77585
- applyThemePreset(preset) {
77586
- const currentSlides = this.editor.slides();
77587
- const result = applyThemeToData({
77588
- slides: [...currentSlides],
77589
- theme: this.loader.theme() ?? {},
77590
- themeColorMap: this.loader.themeColorMap() ?? {},
77591
- }, preset.colorScheme, preset.fontScheme, preset.name);
77592
- // Write slides back through the editor (records undo history).
77593
- this.editor.applyReplacement(result.slides, `Apply theme "${preset.name}"`);
77594
- // Update the loader's theme signals so the check-mark and future switches are correct.
77595
- this.loader.theme.set(result.theme);
77596
- this.loader.themeColorMap.set(result.themeColorMap);
77597
- this.showThemeGallery.set(false);
77598
- }
77599
- /**
77600
- * Wire the framework-agnostic touch-gesture recogniser to the `<main>` canvas
77601
- * host once it is in the DOM. Mirrors React's `useTouchGestures` wiring:
77602
- * - pinch-to-zoom always updates the zoom signal (clamped to the viewer
77603
- * range), with `preventDefault()` on the pinch path to suppress the
77604
- * browser's native pinch-zoom;
77605
- * - horizontal swipe navigates slides, but only when editing is off
77606
- * (`!canEdit()`): in edit mode single-finger gestures belong to element
77607
- * manipulation (move/resize/rotate), so we never hijack them. The large
77608
- * ‹ › buttons remain available for explicit navigation in all modes;
77609
- * - long-press in edit mode opens the editor context menu at the press
77610
- * point for the current selection (mirrors React's onLongPress path).
77611
- *
77612
- * The recogniser's swipe/long-press callbacks check the live `canEdit()` /
77613
- * selection state, so a single attach handles every mode without re-binding.
77614
- */
77615
- setupTouchGestures() {
77616
- const destroyRef = inject(DestroyRef);
77617
- afterNextRender(() => {
77618
- const el = this.mainEl()?.nativeElement;
77619
- if (!el) {
77620
- return;
77621
- }
77622
- const teardown = attachTouchGestures(el, {
77623
- getScale: () => this.zoom(),
77624
- callbacks: {
77625
- onPinchZoom: (newScale) => this.zoom.set(newScale),
77626
- onSwipe: (direction) => {
77627
- // Edit mode: leave single-finger gestures to element manipulation.
77628
- if (this.canEdit()) {
77629
- return;
77630
- }
77631
- // direction 1 = swipe right (previous), -1 = swipe left (next).
77632
- if (direction === 1) {
77633
- this.goPrev();
77634
- }
77635
- else {
77636
- this.goNext();
77637
- }
77638
- },
77639
- onLongPress: (x, y) => {
77640
- if (!this.canEdit() || this.presenting()) {
77641
- return;
77642
- }
77643
- const selected = this.selectedElement();
77644
- if (!selected) {
77645
- return;
77646
- }
77647
- this.contextMenuPos.set({ x, y });
77648
- },
77649
- },
77650
- });
77651
- destroyRef.onDestroy(teardown);
77652
- });
77653
- }
77654
- zoomIn() {
77655
- this.zoom.set(Math.min(ZOOM_MAX, Number((this.zoom() + ZOOM_STEP).toFixed(2))));
77656
- }
77657
- zoomOut() {
77658
- this.zoom.set(Math.max(ZOOM_MIN, Number((this.zoom() - ZOOM_STEP).toFixed(2))));
77659
- }
77660
- zoomReset() {
77661
- this.zoom.set(1);
77662
- }
77663
- /** Open the fullscreen presentation overlay from the current slide. */
77664
- present() {
77665
- if (this.slideCount() > 0) {
77666
- // Deselect first so no edit chrome (selection outline / resize + rotate
77667
- // "Adjust shape" handles) leaks over the slideshow.
77668
- this.editor.clearSelection();
77669
- this.editingId.set(null);
77670
- this.presenting.set(true);
77671
- }
77672
- }
77673
- /**
77674
- * Map a presentation-overlay index back to the full-deck `activeSlideIndex`.
77675
- * The overlay's index is relative to {@link presentationSlides} (a custom show
77676
- * may filter/reorder the deck), so resolve by slide id to keep the editor
77677
- * selection correct when the show closes.
77678
- */
77679
- onPresentationIndexChange(index) {
77680
- const target = this.customShowsCtl.presentationSlides()[index];
77681
- if (!target) {
77682
- return;
77683
- }
77684
- const fullIndex = this.loader.slides().findIndex((s) => s.id === target.id);
77685
- this.activeSlideIndex.set(fullIndex >= 0 ? fullIndex : index);
77686
- }
77687
- /**
77688
- * Open a separate audience tab and hand off the deck via the shared
77689
- * IndexedDB store. Mirrors React's presenter "open audience window".
77690
- */
77691
- openAudienceWindow() {
77692
- const content = this.contentOverride() ?? this.content();
77693
- this.presenterWindow.openAudienceWindow(content, this.activeSlideIndex());
77694
- }
77695
- /** Open the presenter (speaker) view: current+next slide, notes, timer. */
77696
- presentPresenter() {
77697
- if (this.slideCount() > 0) {
77698
- this.presenterStartTime.set(Date.now());
77699
- this.presentingPresenter.set(true);
77700
- }
77701
- }
77702
- /** Close the presenter view (and any audience overlay/window it opened). */
77703
- exitPresenter() {
77704
- this.presentingPresenter.set(false);
77705
- this.presenting.set(false);
77706
- this.presenterWindow.closeAudienceWindow();
77707
- }
77708
78593
  /** Review ▸ Compare: pick a `.pptx` and diff it against the current deck. */
77709
78594
  onOpenCompare() {
77710
78595
  this.compareSvc.startCompare();
77711
78596
  }
77712
- /**
77713
- * Double-click text edit entry: equations open the equation editor instead
77714
- * of the inline text editor (mirrors React's dbl-click-to-edit-equation).
77715
- */
77716
- onTextEditStart(id) {
77717
- const element = this.activeSlide()?.elements.find((el) => el.id === id);
77718
- const segments = element && 'textSegments' in element ? element.textSegments : undefined;
77719
- const equation = segments?.find((segment) => segment.equationXml);
77720
- if (this.canEdit() && equation?.equationXml) {
77721
- this.dialogs.openEquationEdit(id, equation.equationXml);
77722
- return;
77723
- }
77724
- this.editingId.set(id);
77725
- }
77726
- /** Apply a Ctrl/Cmd+B/I/U toggle from the inline editor (undoable). */
77727
- onTextFormat(event) {
77728
- if (!this.canEdit()) {
77729
- return;
77730
- }
77731
- const element = this.activeSlide()?.elements.find((el) => el.id === event.id);
77732
- if (!element) {
77733
- return;
77734
- }
77735
- this.editor.updateElement(this.activeSlideIndex(), event.id, textStylePatch(element, event.updates));
77736
- }
77737
- /** Presentation exited with ink on it: offer the keep/discard prompt. */
77738
- onPresentationAnnotationsExit(map) {
77739
- if (this.canEdit()) {
77740
- this.extraDialogs()?.promptKeepAnnotations(map);
77741
- }
77742
- }
77743
78597
  /** Swap the deck for a restored version-history snapshot. */
77744
78598
  onRestoreVersion(bytes) {
77745
- this.contentOverride.set(bytes);
77746
- }
77747
- /** Toggle the speaker-notes strip. */
77748
- toggleNotes() {
77749
- this.showNotes.update((v) => !v);
77750
- }
77751
- /**
77752
- * File ▸ Open: host override (`onOpenFile` input) takes precedence; otherwise
77753
- * a built-in native picker loads the chosen presentation in place.
77754
- */
77755
- openFile() {
77756
- const override = this.onOpenFile();
77757
- if (override) {
77758
- override();
77759
- return;
77760
- }
77761
- void (async () => {
77762
- const picked = await openPptxFile();
77763
- if (picked) {
77764
- this.contentOverride.set(new Uint8Array(picked.buffer));
77765
- }
77766
- })();
77767
- }
77768
- // ── Mobile notes swipe-to-dismiss ─────────────────────────────────────────
77769
- // The notes sheet stays in normal flow (see template/CSS notes), so the drag
77770
- // gesture is wired here rather than via the fixed-overlay `pptx-mobile-sheet`.
77771
- /** Live downward drag offset for the notes sheet (px; 0 when idle). */
77772
- notesDragY = signal(0, /* @ts-ignore */
77773
- ...(ngDevMode ? [{ debugName: "notesDragY" }] : /* istanbul ignore next */ []));
77774
- /** True while a notes-sheet drag is in progress (disables the snap-back transition). */
77775
- notesDragging = signal(false, /* @ts-ignore */
77776
- ...(ngDevMode ? [{ debugName: "notesDragging" }] : /* istanbul ignore next */ []));
77777
- notesDragStartY = null;
77778
- onNotesPointerDown(event) {
77779
- this.notesDragStartY = event.clientY;
77780
- this.notesDragging.set(true);
77781
- event.target.setPointerCapture?.(event.pointerId);
77782
- }
77783
- onNotesPointerMove(event) {
77784
- if (this.notesDragStartY === null) {
77785
- return;
77786
- }
77787
- this.notesDragY.set(Math.max(0, event.clientY - this.notesDragStartY));
77788
- }
77789
- onNotesPointerUp(event) {
77790
- if (this.notesDragStartY === null) {
77791
- return;
77792
- }
77793
- const delta = event.clientY - this.notesDragStartY;
77794
- this.notesDragStartY = null;
77795
- this.notesDragging.set(false);
77796
- event.target.releasePointerCapture?.(event.pointerId);
77797
- // 120 px matches `pptx-mobile-sheet`'s DISMISS_THRESHOLD for consistency.
77798
- if (delta > 120) {
77799
- this.showNotes.set(false);
77800
- }
77801
- this.notesDragY.set(0);
77802
- }
77803
- // ── Mobile inspector (Format/Comments/Selection/…) swipe-to-dismiss ─────────
77804
- // The inspector host docks in-flow below the canvas on mobile (same keyboard-
77805
- // reachability reason as the notes sheet), so the swipe gesture is wired here.
77806
- /** True once the user swiped the inspector away on mobile (until reopened). */
77807
- mobileInspectorHidden = signal(false, /* @ts-ignore */
77808
- ...(ngDevMode ? [{ debugName: "mobileInspectorHidden" }] : /* istanbul ignore next */ []));
77809
- /** Live downward drag offset for the inspector host (px; 0 when idle). */
77810
- inspectorDragY = signal(0, /* @ts-ignore */
77811
- ...(ngDevMode ? [{ debugName: "inspectorDragY" }] : /* istanbul ignore next */ []));
77812
- /** True while an inspector-host drag is in progress. */
77813
- inspectorDragging = signal(false, /* @ts-ignore */
77814
- ...(ngDevMode ? [{ debugName: "inspectorDragging" }] : /* istanbul ignore next */ []));
77815
- inspectorDragStartY = null;
77816
- onInspectorPointerDown(event) {
77817
- this.inspectorDragStartY = event.clientY;
77818
- this.inspectorDragging.set(true);
77819
- event.target.setPointerCapture?.(event.pointerId);
77820
- }
77821
- onInspectorPointerMove(event) {
77822
- if (this.inspectorDragStartY === null) {
77823
- return;
77824
- }
77825
- this.inspectorDragY.set(Math.max(0, event.clientY - this.inspectorDragStartY));
77826
- }
77827
- onInspectorPointerUp(event) {
77828
- if (this.inspectorDragStartY === null) {
77829
- return;
77830
- }
77831
- const delta = event.clientY - this.inspectorDragStartY;
77832
- this.inspectorDragStartY = null;
77833
- this.inspectorDragging.set(false);
77834
- event.target.releasePointerCapture?.(event.pointerId);
77835
- if (delta > 120) {
77836
- this.mobileInspectorHidden.set(true);
77837
- this.activePanel.set(null);
77838
- }
77839
- this.inspectorDragY.set(0);
77840
- }
77841
- /**
77842
- * Mobile quick-insert: drop a text box on the active slide. Mirrors React's
77843
- * mobile bottom-bar "Insert" slot (a text box is the most common starter
77844
- * element on a phone; the full Insert section lives in the top-bar menu).
77845
- */
77846
- onMobileInsert() {
77847
- if (!this.canEdit() || this.slideCount() === 0) {
77848
- return;
77849
- }
77850
- // Close any open mobile sheet so the new element is visible on the canvas.
77851
- this.mobileSheet.set(null);
77852
- this.editor.addElement(this.activeSlideIndex(), newTextElement());
78599
+ this.fileIO.contentOverride.set(bytes);
77853
78600
  }
77854
78601
  /**
77855
78602
  * Mobile "Format" slot: surface the inspector for the current selection. The
@@ -77859,16 +78606,10 @@ class PowerPointViewerComponent {
77859
78606
  * instead).
77860
78607
  */
77861
78608
  onMobileFormat() {
77862
- this.activePanel.set(null);
77863
- this.mobileSheet.set(null);
78609
+ this.inspectorPanel.activePanel.set(null);
78610
+ this.mobileSheetSvc.mobileSheet.set(null);
77864
78611
  // Reopen the inspector if a prior swipe-down had dismissed it.
77865
- this.mobileInspectorHidden.set(false);
77866
- }
77867
- /** Toggle a right-docked tool panel (clicking the active one closes it). */
77868
- togglePanel(panel) {
77869
- this.activePanel.update((current) => (current === panel ? null : panel));
77870
- // Tapping a panel button re-opens the host even after a swipe-dismiss.
77871
- this.mobileInspectorHidden.set(false);
78612
+ this.inspectorPanel.mobileInspectorHidden.set(false);
77872
78613
  }
77873
78614
  /** Receive draw-tool state changes from the ribbon Draw tab. */
77874
78615
  onDrawToolChange(state) {
@@ -77876,21 +78617,6 @@ class PowerPointViewerComponent {
77876
78617
  this.activeDrawColor.set(state.color);
77877
78618
  this.activeDrawWidth.set(state.width);
77878
78619
  }
77879
- /** Receive a completed ink stroke and append it to the active slide. */
77880
- onInkStrokeComplete(ink) {
77881
- if (!this.canEdit()) {
77882
- return;
77883
- }
77884
- this.editor.addElement(this.activeSlideIndex(), ink);
77885
- }
77886
- /** Receive an eraser hit and delete the targeted ink element. */
77887
- onEraserHit(id) {
77888
- if (!this.canEdit()) {
77889
- return;
77890
- }
77891
- this.editor.select([id]);
77892
- this.editor.deleteSelected(this.activeSlideIndex());
77893
- }
77894
78620
  /** Append a comment to the active slide (one history entry). */
77895
78621
  onCommentAdd(text) {
77896
78622
  const next = addCommentToList(this.activeComments(), text, 'You');
@@ -77912,94 +78638,6 @@ class PowerPointViewerComponent {
77912
78638
  this.editor.updateSlide(this.activeSlideIndex(), { comments: next });
77913
78639
  }
77914
78640
  }
77915
- /**
77916
- * Persist a document-properties edit from the Info dialog. Gated on
77917
- * {@link canEdit}: viewers may inspect properties but not mutate them
77918
- * (mirrors the comments / hyperlink edit paths).
77919
- */
77920
- onPropertiesSave(patch) {
77921
- if (!this.canEdit()) {
77922
- this.showProperties.set(false);
77923
- return;
77924
- }
77925
- this.coreOverride.update((current) => ({ ...current, ...patch }));
77926
- this.propertiesChange.emit(patch);
77927
- this.showProperties.set(false);
77928
- }
77929
- /** Apply a hyperlink edit to the selected element (one history entry). */
77930
- onHyperlinkSave(patch) {
77931
- const el = this.selectedElement();
77932
- if (el) {
77933
- this.editor.updateElement(this.activeSlideIndex(), el.id, patch);
77934
- }
77935
- this.showHyperlink.set(false);
77936
- }
77937
- /**
77938
- * Handle an element press from the canvas. Additive (Shift/Ctrl) toggles
77939
- * membership; a plain press selects the element (keeping it selected if it
77940
- * already was, so a subsequent drag works).
77941
- */
77942
- onElementSelect(event) {
77943
- // The armed format painter intercepts the next element click: apply the
77944
- // copied format to the target, then disarm (no selection change).
77945
- if (this.formatPainter.active()) {
77946
- this.formatPainter.applyToTarget(event.id);
77947
- this.formatPainter.cancel();
77948
- return;
77949
- }
77950
- if (event.additive) {
77951
- this.editor.toggleSelect(event.id, true);
77952
- }
77953
- else if (!this.editor.isSelected(event.id)) {
77954
- this.editor.select([event.id]);
77955
- }
77956
- }
77957
- /** Empty-stage press: disarm the painter if armed, else clear the selection. */
77958
- onBackgroundClick() {
77959
- if (this.formatPainter.active()) {
77960
- this.formatPainter.cancel();
77961
- return;
77962
- }
77963
- this.editor.clearSelection();
77964
- }
77965
- /** Right-click: select the element under the cursor and open the menu. */
77966
- onContextMenu(event) {
77967
- if (event.id && !this.editor.isSelected(event.id)) {
77968
- this.editor.select([event.id]);
77969
- }
77970
- this.contextMenuPos.set({ x: event.x, y: event.y });
77971
- }
77972
- /** Update the active slide's background colour. */
77973
- onSlideBackground(event) {
77974
- this.editor.updateSlide(this.activeSlideIndex(), {
77975
- backgroundColor: event.target.value,
77976
- });
77977
- }
77978
- /** Update the active slide's speaker notes. */
77979
- onSlideNotes(event) {
77980
- this.editor.updateSlide(this.activeSlideIndex(), {
77981
- notes: event.target.value,
77982
- });
77983
- }
77984
- /** Update the active slide's speaker notes from the editable NotesPanel. */
77985
- onNotesUpdate(notes) {
77986
- this.editor.updateSlide(this.activeSlideIndex(), { notes });
77987
- }
77988
- // ── Selection pane handlers ────────────────────────────────────────────────
77989
- onSelectionPaneBringForward(id) {
77990
- this.editor.select([id]);
77991
- this.editor.bringSelectedForward(this.activeSlideIndex());
77992
- }
77993
- onSelectionPaneSendBackward(id) {
77994
- this.editor.select([id]);
77995
- this.editor.sendSelectedBackward(this.activeSlideIndex());
77996
- }
77997
- onToggleElementHidden(id) {
77998
- const el = this.activeSlide()?.elements.find((e) => e.id === id);
77999
- if (el) {
78000
- this.editor.updateElement(this.activeSlideIndex(), id, { hidden: !el.hidden });
78001
- }
78002
- }
78003
78641
  // ── Insert SmartArt ────────────────────────────────────────────────────────
78004
78642
  /**
78005
78643
  * Insert a new SmartArt element built from the dialog's chosen preset + item
@@ -78011,44 +78649,6 @@ class PowerPointViewerComponent {
78011
78649
  this.editor.addElement(this.activeSlideIndex(), element);
78012
78650
  this.showSmartArtInsert.set(false);
78013
78651
  }
78014
- /** Commit an inline text edit: replace the element's text (one history entry). */
78015
- onTextCommit(event) {
78016
- this.editor.updateElement(this.activeSlideIndex(), event.id, {
78017
- text: event.text,
78018
- textSegments: [],
78019
- });
78020
- this.editingId.set(null);
78021
- }
78022
- /**
78023
- * Commit a table cell's inline text edit. Finds the table element on the
78024
- * active slide, rebuilds its `tableData` with the new cell text, and patches
78025
- * it through the editor (which records undo history).
78026
- */
78027
- onTableCellCommit(event) {
78028
- if (!this.canEdit()) {
78029
- return;
78030
- }
78031
- const el = this.activeSlide()?.elements.find((e) => e.id === event.id);
78032
- if (!el || el.type !== 'table') {
78033
- return;
78034
- }
78035
- const updated = setCellText(el, event.commit.rowIndex, event.commit.colIndex, event.commit.text);
78036
- this.editor.updateElement(this.activeSlideIndex(), event.id, {
78037
- tableData: updated.tableData,
78038
- });
78039
- }
78040
- /**
78041
- * Persist a structural table change originating on the canvas (column / row
78042
- * drag-resize) as one undoable history entry.
78043
- */
78044
- onTableChange(event) {
78045
- if (!this.canEdit()) {
78046
- return;
78047
- }
78048
- this.editor.updateElement(this.activeSlideIndex(), event.id, {
78049
- tableData: event.tableData,
78050
- });
78051
- }
78052
78652
  /**
78053
78653
  * Editing keyboard shortcuts (only when `canEdit` and not typing in a
78054
78654
  * field or presenting). The decorator must live on the component; the logic
@@ -78081,8 +78681,18 @@ class PowerPointViewerComponent {
78081
78681
  ViewerFindReplaceService,
78082
78682
  ViewerCustomShowsService,
78083
78683
  ViewerCollaborationSessionService,
78684
+ ViewerCanvasEditingService,
78685
+ ViewerCollabCursorService,
78686
+ ViewerDocumentPropertiesService,
78687
+ ViewerFileIOService,
78084
78688
  ViewerFormatPainterService,
78689
+ ViewerInspectorPanelService,
78085
78690
  ViewerKeyboardService,
78691
+ ViewerMobileSheetService,
78692
+ ViewerPresentationModeService,
78693
+ ViewerThemeGalleryService,
78694
+ ViewerTouchGesturesService,
78695
+ ViewerZoomService,
78086
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: `
78087
78697
  <div class="pptx-ng-viewer" [ngClass]="class()" [ngStyle]="rootStyle()">
78088
78698
  @if (loader.loading()) {
@@ -78106,13 +78716,13 @@ class PowerPointViewerComponent {
78106
78716
  [slideCount]="slideCount()"
78107
78717
  [canEdit]="canEdit()"
78108
78718
  [selectedElement]="selectedElement()"
78109
- [zoomPercent]="zoomPercent()"
78719
+ [zoomPercent]="zoomSvc.zoomPercent()"
78110
78720
  [formatPainterActive]="formatPainter.active()"
78111
78721
  [canActivateFormatPainter]="formatPainter.canActivate()"
78112
78722
  [exporting]="xport.exporting()"
78113
78723
  [sidebarCollapsed]="slidesPanelCollapsed()"
78114
- [inspectorOpen]="inspectorPaneOpen()"
78115
- [commentsOpen]="activePanel() === 'comments'"
78724
+ [inspectorOpen]="inspectorPanel.inspectorPaneOpen()"
78725
+ [commentsOpen]="inspectorPanel.activePanel() === 'comments'"
78116
78726
  [commentCount]="activeComments().length"
78117
78727
  [findOpen]="findReplace.showFind() || findReplace.showFindReplace()"
78118
78728
  [collabConnected]="collab.connected()"
@@ -78120,31 +78730,31 @@ class PowerPointViewerComponent {
78120
78730
  (toggleSidebar)="slidesPanelCollapsed.update(v => !v)"
78121
78731
  (prev)="goPrev()"
78122
78732
  (next)="goNext()"
78123
- (zoomIn)="zoomIn()"
78124
- (zoomOut)="zoomOut()"
78125
- (zoomReset)="zoomReset()"
78733
+ (zoomIn)="zoomSvc.zoomIn()"
78734
+ (zoomOut)="zoomSvc.zoomOut()"
78735
+ (zoomReset)="zoomSvc.zoomReset()"
78126
78736
  (find)="findReplace.showFind.set(true)"
78127
- (present)="present()"
78128
- (presenter)="presentPresenter()"
78737
+ (present)="presentationMode.present()"
78738
+ (presenter)="presentationMode.presentPresenter()"
78129
78739
  (share)="session.showShare.set(true)"
78130
78740
  (broadcast)="session.showBroadcast.set(true)"
78131
- (openFile)="openFile()"
78132
- (save)="saveAsPptx()"
78133
- (info)="showProperties.set(true)"
78741
+ (openFile)="fileIO.openFile()"
78742
+ (save)="fileIO.saveAsPptx()"
78743
+ (info)="docProperties.showProperties.set(true)"
78134
78744
  (print)="print.openDialog()"
78135
- (comments)="togglePanel('comments')"
78136
- (signatures)="togglePanel('signatures')"
78137
- (a11y)="togglePanel('accessibility')"
78138
- (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)"
78139
78749
  (openSorter)="showSorter.set(true)"
78140
- (toggleNotes)="toggleNotes()"
78750
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
78141
78751
  (toggleFormatPainter)="formatPainter.toggle()"
78142
78752
  (exportPng)="xport.exportPng()"
78143
78753
  (exportPdf)="xport.exportPdf()"
78144
78754
  (exportGif)="xport.exportGif()"
78145
78755
  (exportVideo)="xport.exportVideo()"
78146
78756
  (replace)="findReplace.openFindReplace()"
78147
- (toggleInspector)="activePanel.set(null)"
78757
+ (toggleInspector)="inspectorPanel.activePanel.set(null)"
78148
78758
  (drawToolChange)="onDrawToolChange($event)"
78149
78759
  [showGrid]="showGrid()"
78150
78760
  [showRulers]="showRulers()"
@@ -78156,9 +78766,9 @@ class PowerPointViewerComponent {
78156
78766
  (toggleGuides)="showGuides.update(v => !v)"
78157
78767
  (toggleSnapToGrid)="snapToGrid.update(v => !v)"
78158
78768
  (toggleEyedropper)="formatPainter.toggleEyedropper()"
78159
- [themeGalleryOpen]="showThemeGallery()"
78160
- (toggleThemeGallery)="showThemeGallery.update(v => !v)"
78161
- (toggleSelectionPane)="togglePanel('selection')"
78769
+ [themeGalleryOpen]="themeGallery.showThemeGallery()"
78770
+ (toggleThemeGallery)="themeGallery.showThemeGallery.update(v => !v)"
78771
+ (toggleSelectionPane)="inspectorPanel.togglePanel('selection')"
78162
78772
  (openCustomShows)="customShowsCtl.showDialog.set(true)"
78163
78773
  (openSmartArtDialog)="showSmartArtInsert.set(true)"
78164
78774
  (openEquationDialog)="dialogs.openEquationInsert()"
@@ -78177,12 +78787,12 @@ class PowerPointViewerComponent {
78177
78787
  [canUndo]="editor.canUndo()"
78178
78788
  [canRedo]="editor.canRedo()"
78179
78789
  [canPresent]="slideCount() > 0"
78180
- [menuOpen]="mobileSheet() === 'menu'"
78181
- (toggleMenu)="mobileSheet.set(mobileSheet() === 'menu' ? null : 'menu')"
78790
+ [menuOpen]="mobileSheetSvc.mobileSheet() === 'menu'"
78791
+ (toggleMenu)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'menu' ? null : 'menu')"
78182
78792
  (undo)="editor.undo()"
78183
78793
  (redo)="editor.redo()"
78184
- (save)="saveAsPptx()"
78185
- (present)="present()"
78794
+ (save)="fileIO.saveAsPptx()"
78795
+ (present)="presentationMode.present()"
78186
78796
  />
78187
78797
  }
78188
78798
 
@@ -78209,12 +78819,12 @@ class PowerPointViewerComponent {
78209
78819
  </nav>
78210
78820
  }
78211
78821
 
78212
- <main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
78822
+ <main class="pptx-ng-main" #mainEl (pointermove)="collabCursor.onPointerMove($event)">
78213
78823
  <pptx-slide-canvas
78214
78824
  [slide]="activeSlide()"
78215
78825
  [canvasSize]="loader.canvasSize()"
78216
78826
  [mediaDataUrls]="loader.mediaDataUrls()"
78217
- [zoom]="zoom()"
78827
+ [zoom]="zoomSvc.zoom()"
78218
78828
  [editable]="canEdit()"
78219
78829
  [selectedIds]="editor.selectedIds()"
78220
78830
  [showGrid]="showGrid()"
@@ -78227,32 +78837,32 @@ class PowerPointViewerComponent {
78227
78837
  [drawWidth]="activeDrawWidth()"
78228
78838
  [editTemplateMode]="editor.editTemplateMode()"
78229
78839
  [templateElements]="activeTemplateElements()"
78230
- (elementSelect)="onElementSelect($event)"
78231
- (backgroundClick)="onBackgroundClick()"
78840
+ (elementSelect)="canvasEditing.onElementSelect($event)"
78841
+ (backgroundClick)="canvasEditing.onBackgroundClick()"
78232
78842
  (marqueeSelect)="editor.select($event)"
78233
78843
  (transformStart)="editor.beginTransform($event.label)"
78234
78844
  (transformUpdate)="editor.applyTransform(activeSlideIndex(), $event.id, $event.box)"
78235
78845
  (rotateUpdate)="
78236
78846
  editor.applyTransform(activeSlideIndex(), $event.id, { rotation: $event.rotation })
78237
78847
  "
78238
- (contextMenu)="onContextMenu($event)"
78239
- [editingId]="editingId()"
78240
- (textEditStart)="onTextEditStart($event.id)"
78241
- (textCommit)="onTextCommit($event)"
78242
- (textCancel)="editingId.set(null)"
78243
- (textFormat)="onTextFormat($event)"
78244
- (inkStrokeComplete)="onInkStrokeComplete($event)"
78245
- (eraserHit)="onEraserHit($event)"
78246
- (cellCommit)="onTableCellCommit($event)"
78247
- (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)"
78248
78858
  />
78249
78859
  @if (collab.connected()) {
78250
- <pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
78860
+ <pptx-collaboration-cursors [cursors]="collabCursor.cursors()" [zoom]="zoomSvc.zoom()" />
78251
78861
  <pptx-remote-selection-overlay
78252
78862
  [presences]="collab.presence()"
78253
78863
  [elements]="activeSlide()?.elements ?? []"
78254
78864
  [activeSlideIndex]="activeSlideIndex()"
78255
- [zoom]="zoom()"
78865
+ [zoom]="zoomSvc.zoom()"
78256
78866
  />
78257
78867
  }
78258
78868
  @if (collab.active() && collab.presence().length > 0) {
@@ -78264,11 +78874,11 @@ class PowerPointViewerComponent {
78264
78874
  />
78265
78875
  </div>
78266
78876
  }
78267
- @if (showNotes() && !mobile.isMobile()) {
78877
+ @if (mobileSheetSvc.showNotes() && !mobile.isMobile()) {
78268
78878
  <aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
78269
78879
  <pptx-notes-panel
78270
78880
  [slide]="activeSlide()"
78271
- (update)="onNotesUpdate($event)"
78881
+ (update)="canvasEditing.onNotesUpdate($event)"
78272
78882
  />
78273
78883
  </aside>
78274
78884
  }
@@ -78277,25 +78887,26 @@ class PowerPointViewerComponent {
78277
78887
  <!--
78278
78888
  Single inspector host for every right-rail panel. On mobile it docks
78279
78889
  full-width below the canvas and is swipe-dismissable (the grab handle
78280
- feeds onInspectorPointerDown/Move/Up); a downward swipe past the
78281
- 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.
78282
78893
  -->
78283
- @if (visibleInspectorKind(); as kind) {
78894
+ @if (inspectorPanel.visibleInspectorKind(); as kind) {
78284
78895
  <aside
78285
78896
  class="pptx-ng-inspector-host"
78286
- [attr.aria-label]="inspectorLabel() | translate"
78897
+ [attr.aria-label]="inspectorPanel.inspectorLabel() | translate"
78287
78898
  [style.transform]="
78288
- inspectorDragY() > 0 ? 'translateY(' + inspectorDragY() + 'px)' : null
78899
+ inspectorPanel.inspectorDrag.dragY() > 0 ? 'translateY(' + inspectorPanel.inspectorDrag.dragY() + 'px)' : null
78289
78900
  "
78290
- [style.transition]="inspectorDragging() ? 'none' : 'transform 150ms ease-out'"
78901
+ [style.transition]="inspectorPanel.inspectorDrag.dragging() ? 'none' : 'transform 150ms ease-out'"
78291
78902
  >
78292
78903
  <!-- Swipe-down-to-dismiss grab handle (mobile only; hidden on desktop). -->
78293
78904
  <div
78294
78905
  class="pptx-ng-idrawer-grab"
78295
- (pointerdown)="onInspectorPointerDown($event)"
78296
- (pointermove)="onInspectorPointerMove($event)"
78297
- (pointerup)="onInspectorPointerUp($event)"
78298
- (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)"
78299
78910
  >
78300
78911
  <div class="pptx-ng-idrawer-handle"></div>
78301
78912
  </div>
@@ -78322,9 +78933,9 @@ class PowerPointViewerComponent {
78322
78933
  [elements]="activeSlide()?.elements ?? []"
78323
78934
  [selectedIds]="editor.selectedIds()"
78324
78935
  (selectElement)="editor.select([$event])"
78325
- (bringForward)="onSelectionPaneBringForward($event)"
78326
- (sendBackward)="onSelectionPaneSendBackward($event)"
78327
- (toggleHidden)="onToggleElementHidden($event)"
78936
+ (bringForward)="canvasEditing.onSelectionPaneBringForward($event)"
78937
+ (sendBackward)="canvasEditing.onSelectionPaneSendBackward($event)"
78938
+ (toggleHidden)="canvasEditing.onToggleElementHidden($event)"
78328
78939
  />
78329
78940
  }
78330
78941
  @case ('element') {
@@ -78348,7 +78959,7 @@ class PowerPointViewerComponent {
78348
78959
  <input
78349
78960
  type="color"
78350
78961
  [attr.value]="sl.backgroundColor || '#ffffff'"
78351
- (change)="onSlideBackground($event)"
78962
+ (change)="canvasEditing.onSlideBackground($event)"
78352
78963
  />
78353
78964
  </label>
78354
78965
  <label class="pptx-ng-prop-row pptx-ng-prop-col">
@@ -78356,8 +78967,8 @@ class PowerPointViewerComponent {
78356
78967
  <textarea
78357
78968
  rows="5"
78358
78969
  [attr.placeholder]="'pptx.viewer.speakerNotesPlaceholder' | translate"
78359
- (change)="onSlideNotes($event)"
78360
- (blur)="onSlideNotes($event)"
78970
+ (change)="canvasEditing.onSlideNotes($event)"
78971
+ (blur)="canvasEditing.onSlideNotes($event)"
78361
78972
  >{{ sl.notes || '' }}</textarea
78362
78973
  >
78363
78974
  </label>
@@ -78376,17 +78987,17 @@ class PowerPointViewerComponent {
78376
78987
  [slideCount]="slideCount()"
78377
78988
  [canEdit]="canEdit()"
78378
78989
  [dirty]="editor.dirty()"
78379
- [notesOpen]="showNotes()"
78380
- [zoomPercent]="zoomPercent()"
78990
+ [notesOpen]="mobileSheetSvc.showNotes()"
78991
+ [zoomPercent]="zoomSvc.zoomPercent()"
78381
78992
  [sorterActive]="showSorter()"
78382
- [presenting]="presenting()"
78383
- (toggleNotes)="toggleNotes()"
78993
+ [presenting]="presentationMode.presenting()"
78994
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
78384
78995
  (normalView)="showSorter.set(false)"
78385
78996
  (openSorter)="showSorter.set(true)"
78386
- (slideShow)="present()"
78387
- (zoomIn)="zoomIn()"
78388
- (zoomOut)="zoomOut()"
78389
- (zoomReset)="zoomReset()"
78997
+ (slideShow)="presentationMode.present()"
78998
+ (zoomIn)="zoomSvc.zoomIn()"
78999
+ (zoomOut)="zoomSvc.zoomOut()"
79000
+ (zoomReset)="zoomSvc.zoomReset()"
78390
79001
  />
78391
79002
  }
78392
79003
  }
@@ -78402,19 +79013,19 @@ class PowerPointViewerComponent {
78402
79013
  />
78403
79014
  }
78404
79015
 
78405
- @if (presenting()) {
79016
+ @if (presentationMode.presenting()) {
78406
79017
  <pptx-presentation-overlay
78407
79018
  [slides]="customShowsCtl.presentationSlides()"
78408
79019
  [canvasSize]="loader.canvasSize()"
78409
79020
  [mediaDataUrls]="loader.mediaDataUrls()"
78410
79021
  [startIndex]="customShowsCtl.presentationStartIndex()"
78411
- (indexChange)="onPresentationIndexChange($event)"
78412
- (annotationsExit)="onPresentationAnnotationsExit($event)"
78413
- (closed)="presenting.set(false)"
79022
+ (indexChange)="presentationMode.onPresentationIndexChange($event)"
79023
+ (annotationsExit)="presentationMode.onPresentationAnnotationsExit($event)"
79024
+ (closed)="presentationMode.presenting.set(false)"
78414
79025
  />
78415
79026
  }
78416
79027
 
78417
- @if (presentingPresenter()) {
79028
+ @if (presentationMode.presentingPresenter()) {
78418
79029
  @if (mobile.isMobile()) {
78419
79030
  <!-- Single-column mobile presenter layout (phones / landscape phones). -->
78420
79031
  <pptx-mobile-presenter-view
@@ -78422,9 +79033,9 @@ class PowerPointViewerComponent {
78422
79033
  [currentSlideIndex]="activeSlideIndex()"
78423
79034
  [canvasSize]="loader.canvasSize()"
78424
79035
  [mediaDataUrls]="loader.mediaDataUrls()"
78425
- [presentationStartTime]="presenterStartTime()"
79036
+ [presentationStartTime]="presentationMode.presenterStartTime()"
78426
79037
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
78427
- (exit)="exitPresenter()"
79038
+ (exit)="presentationMode.exitPresenter()"
78428
79039
  />
78429
79040
  } @else {
78430
79041
  <pptx-presenter-view
@@ -78432,12 +79043,12 @@ class PowerPointViewerComponent {
78432
79043
  [currentSlideIndex]="activeSlideIndex()"
78433
79044
  [canvasSize]="loader.canvasSize()"
78434
79045
  [mediaDataUrls]="loader.mediaDataUrls()"
78435
- [presentationStartTime]="presenterStartTime()"
79046
+ [presentationStartTime]="presentationMode.presenterStartTime()"
78436
79047
  [isAudienceWindowOpen]="presenterWindow.isAudienceWindowOpen()"
78437
79048
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
78438
- (openAudienceWindow)="openAudienceWindow()"
79049
+ (openAudienceWindow)="presentationMode.openAudienceWindow()"
78439
79050
  (closeAudienceWindow)="presenterWindow.closeAudienceWindow()"
78440
- (exit)="exitPresenter()"
79051
+ (exit)="presentationMode.exitPresenter()"
78441
79052
  />
78442
79053
  }
78443
79054
  }
@@ -78462,27 +79073,27 @@ class PowerPointViewerComponent {
78462
79073
  />
78463
79074
  }
78464
79075
 
78465
- @if (canEdit() && contextMenuPos(); as m) {
79076
+ @if (canEdit() && canvasEditing.contextMenuPos(); as m) {
78466
79077
  <pptx-editor-context-menu
78467
79078
  [x]="m.x"
78468
79079
  [y]="m.y"
78469
79080
  [slideIndex]="activeSlideIndex()"
78470
- (closed)="contextMenuPos.set(null)"
79081
+ (closed)="canvasEditing.contextMenuPos.set(null)"
78471
79082
  />
78472
79083
  }
78473
79084
 
78474
79085
  <pptx-theme-gallery
78475
- [open]="showThemeGallery()"
78476
- [activeName]="activeThemeName()"
78477
- (applyTheme)="applyThemePreset($event)"
78478
- (close)="showThemeGallery.set(false)"
79086
+ [open]="themeGallery.showThemeGallery()"
79087
+ [activeName]="themeGallery.activeThemeName()"
79088
+ (applyTheme)="themeGallery.applyThemePreset($event)"
79089
+ (close)="themeGallery.showThemeGallery.set(false)"
78479
79090
  />
78480
79091
 
78481
79092
  <pptx-properties-dialog
78482
- [open]="showProperties()"
78483
- [properties]="coreProperties()"
78484
- (save)="onPropertiesSave($event)"
78485
- (close)="showProperties.set(false)"
79093
+ [open]="docProperties.showProperties()"
79094
+ [properties]="docProperties.coreProperties()"
79095
+ (save)="docProperties.onPropertiesSave($event)"
79096
+ (close)="docProperties.showProperties.set(false)"
78486
79097
  />
78487
79098
 
78488
79099
  <!-- Secondary dialogs / side panels (equation, set-up show, password,
@@ -78498,10 +79109,10 @@ class PowerPointViewerComponent {
78498
79109
 
78499
79110
  @if (canEdit()) {
78500
79111
  <pptx-hyperlink-dialog
78501
- [open]="showHyperlink()"
79112
+ [open]="docProperties.showHyperlink()"
78502
79113
  [element]="selectedElement()"
78503
- (save)="onHyperlinkSave($event)"
78504
- (close)="showHyperlink.set(false)"
79114
+ (save)="docProperties.onHyperlinkSave($event)"
79115
+ (close)="docProperties.showHyperlink.set(false)"
78505
79116
  />
78506
79117
  }
78507
79118
 
@@ -78572,29 +79183,29 @@ class PowerPointViewerComponent {
78572
79183
  <!-- ── Mobile chrome (narrow / touch viewports only) ─────────────── -->
78573
79184
  @if (mobile.isMobile() && !loader.loading() && !loader.error()) {
78574
79185
  <pptx-mobile-slides-sheet
78575
- [open]="mobileSheet() === 'slides'"
79186
+ [open]="mobileSheetSvc.mobileSheet() === 'slides'"
78576
79187
  [slides]="displaySlidesMut()"
78577
79188
  [canvasSize]="loader.canvasSize()"
78578
79189
  [mediaDataUrls]="loader.mediaDataUrls()"
78579
79190
  [activeIndex]="activeSlideIndex()"
78580
79191
  (jumpToSlide)="goTo($event)"
78581
- (closed)="mobileSheet.set(null)"
79192
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
78582
79193
  />
78583
79194
 
78584
79195
  <pptx-mobile-menu-sheet
78585
- [open]="mobileSheet() === 'menu'"
79196
+ [open]="mobileSheetSvc.mobileSheet() === 'menu'"
78586
79197
  [slideCount]="slideCount()"
78587
79198
  [exporting]="xport.exporting()"
78588
- [showNotes]="showNotes()"
79199
+ [showNotes]="mobileSheetSvc.showNotes()"
78589
79200
  [canEdit]="canEdit()"
78590
- (closed)="mobileSheet.set(null)"
79201
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
78591
79202
  (openFind)="findReplace.showFind.set(true)"
78592
79203
  (openSorter)="showSorter.set(true)"
78593
- (toggleNotes)="toggleNotes()"
78594
- (insertText)="onMobileInsert()"
78595
- (present)="present()"
78596
- (openFile)="openFile()"
78597
- (savePptx)="saveAsPptx()"
79204
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
79205
+ (insertText)="mobileSheetSvc.onMobileInsert()"
79206
+ (present)="presentationMode.present()"
79207
+ (openFile)="fileIO.openFile()"
79208
+ (savePptx)="fileIO.saveAsPptx()"
78598
79209
  (exportPng)="xport.exportPng()"
78599
79210
  (exportPdf)="xport.exportPdf()"
78600
79211
  (exportGif)="xport.exportGif()"
@@ -78610,24 +79221,24 @@ class PowerPointViewerComponent {
78610
79221
  the visual viewport ends up below the document on mobile (100vh layout
78611
79222
  viewport < dynamic viewport), leaving its textarea unreachable to taps.
78612
79223
  Mirrors React, where the notes panel is a flow sibling below the canvas. -->
78613
- @if (showNotes()) {
79224
+ @if (mobileSheetSvc.showNotes()) {
78614
79225
  <div
78615
79226
  class="pptx-ng-mobile-notes-sheet"
78616
- [style.transform]="notesDragY() > 0 ? 'translateY(' + notesDragY() + 'px)' : null"
78617
- [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'"
78618
79229
  >
78619
79230
  <!-- Swipe-down-to-dismiss grab handle (kept in-flow so the keyboard
78620
79231
  can't push the textarea out of reach). -->
78621
79232
  <div
78622
79233
  class="pptx-ng-mnotes-grab"
78623
- (pointerdown)="onNotesPointerDown($event)"
78624
- (pointermove)="onNotesPointerMove($event)"
78625
- (pointerup)="onNotesPointerUp($event)"
78626
- (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)"
78627
79238
  >
78628
79239
  <div class="pptx-ng-mnotes-handle"></div>
78629
79240
  </div>
78630
- <pptx-notes-panel [slide]="activeSlide()" (update)="onNotesUpdate($event)" />
79241
+ <pptx-notes-panel [slide]="activeSlide()" (update)="canvasEditing.onNotesUpdate($event)" />
78631
79242
  </div>
78632
79243
  }
78633
79244
 
@@ -78641,11 +79252,11 @@ class PowerPointViewerComponent {
78641
79252
  [slideCount]="slideCount()"
78642
79253
  [commentCount]="activeComments().length"
78643
79254
  [activeSheet]="mobileBarSheet()"
78644
- (openSlides)="mobileSheet.set(mobileSheet() === 'slides' ? null : 'slides')"
78645
- (insert)="onMobileInsert()"
79255
+ (openSlides)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'slides' ? null : 'slides')"
79256
+ (insert)="mobileSheetSvc.onMobileInsert()"
78646
79257
  (openFormat)="onMobileFormat()"
78647
- (openComments)="togglePanel('comments')"
78648
- (notes)="toggleNotes()"
79258
+ (openComments)="inspectorPanel.togglePanel('comments')"
79259
+ (notes)="mobileSheetSvc.toggleNotes()"
78649
79260
  />
78650
79261
  }
78651
79262
  </div>
@@ -78676,8 +79287,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78676
79287
  ViewerFindReplaceService,
78677
79288
  ViewerCustomShowsService,
78678
79289
  ViewerCollaborationSessionService,
79290
+ ViewerCanvasEditingService,
79291
+ ViewerCollabCursorService,
79292
+ ViewerDocumentPropertiesService,
79293
+ ViewerFileIOService,
78679
79294
  ViewerFormatPainterService,
79295
+ ViewerInspectorPanelService,
78680
79296
  ViewerKeyboardService,
79297
+ ViewerMobileSheetService,
79298
+ ViewerPresentationModeService,
79299
+ ViewerThemeGalleryService,
79300
+ ViewerTouchGesturesService,
79301
+ ViewerZoomService,
78681
79302
  ],
78682
79303
  imports: [
78683
79304
  NgClass,
@@ -78742,13 +79363,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78742
79363
  [slideCount]="slideCount()"
78743
79364
  [canEdit]="canEdit()"
78744
79365
  [selectedElement]="selectedElement()"
78745
- [zoomPercent]="zoomPercent()"
79366
+ [zoomPercent]="zoomSvc.zoomPercent()"
78746
79367
  [formatPainterActive]="formatPainter.active()"
78747
79368
  [canActivateFormatPainter]="formatPainter.canActivate()"
78748
79369
  [exporting]="xport.exporting()"
78749
79370
  [sidebarCollapsed]="slidesPanelCollapsed()"
78750
- [inspectorOpen]="inspectorPaneOpen()"
78751
- [commentsOpen]="activePanel() === 'comments'"
79371
+ [inspectorOpen]="inspectorPanel.inspectorPaneOpen()"
79372
+ [commentsOpen]="inspectorPanel.activePanel() === 'comments'"
78752
79373
  [commentCount]="activeComments().length"
78753
79374
  [findOpen]="findReplace.showFind() || findReplace.showFindReplace()"
78754
79375
  [collabConnected]="collab.connected()"
@@ -78756,31 +79377,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78756
79377
  (toggleSidebar)="slidesPanelCollapsed.update(v => !v)"
78757
79378
  (prev)="goPrev()"
78758
79379
  (next)="goNext()"
78759
- (zoomIn)="zoomIn()"
78760
- (zoomOut)="zoomOut()"
78761
- (zoomReset)="zoomReset()"
79380
+ (zoomIn)="zoomSvc.zoomIn()"
79381
+ (zoomOut)="zoomSvc.zoomOut()"
79382
+ (zoomReset)="zoomSvc.zoomReset()"
78762
79383
  (find)="findReplace.showFind.set(true)"
78763
- (present)="present()"
78764
- (presenter)="presentPresenter()"
79384
+ (present)="presentationMode.present()"
79385
+ (presenter)="presentationMode.presentPresenter()"
78765
79386
  (share)="session.showShare.set(true)"
78766
79387
  (broadcast)="session.showBroadcast.set(true)"
78767
- (openFile)="openFile()"
78768
- (save)="saveAsPptx()"
78769
- (info)="showProperties.set(true)"
79388
+ (openFile)="fileIO.openFile()"
79389
+ (save)="fileIO.saveAsPptx()"
79390
+ (info)="docProperties.showProperties.set(true)"
78770
79391
  (print)="print.openDialog()"
78771
- (comments)="togglePanel('comments')"
78772
- (signatures)="togglePanel('signatures')"
78773
- (a11y)="togglePanel('accessibility')"
78774
- (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)"
78775
79396
  (openSorter)="showSorter.set(true)"
78776
- (toggleNotes)="toggleNotes()"
79397
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
78777
79398
  (toggleFormatPainter)="formatPainter.toggle()"
78778
79399
  (exportPng)="xport.exportPng()"
78779
79400
  (exportPdf)="xport.exportPdf()"
78780
79401
  (exportGif)="xport.exportGif()"
78781
79402
  (exportVideo)="xport.exportVideo()"
78782
79403
  (replace)="findReplace.openFindReplace()"
78783
- (toggleInspector)="activePanel.set(null)"
79404
+ (toggleInspector)="inspectorPanel.activePanel.set(null)"
78784
79405
  (drawToolChange)="onDrawToolChange($event)"
78785
79406
  [showGrid]="showGrid()"
78786
79407
  [showRulers]="showRulers()"
@@ -78792,9 +79413,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78792
79413
  (toggleGuides)="showGuides.update(v => !v)"
78793
79414
  (toggleSnapToGrid)="snapToGrid.update(v => !v)"
78794
79415
  (toggleEyedropper)="formatPainter.toggleEyedropper()"
78795
- [themeGalleryOpen]="showThemeGallery()"
78796
- (toggleThemeGallery)="showThemeGallery.update(v => !v)"
78797
- (toggleSelectionPane)="togglePanel('selection')"
79416
+ [themeGalleryOpen]="themeGallery.showThemeGallery()"
79417
+ (toggleThemeGallery)="themeGallery.showThemeGallery.update(v => !v)"
79418
+ (toggleSelectionPane)="inspectorPanel.togglePanel('selection')"
78798
79419
  (openCustomShows)="customShowsCtl.showDialog.set(true)"
78799
79420
  (openSmartArtDialog)="showSmartArtInsert.set(true)"
78800
79421
  (openEquationDialog)="dialogs.openEquationInsert()"
@@ -78813,12 +79434,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78813
79434
  [canUndo]="editor.canUndo()"
78814
79435
  [canRedo]="editor.canRedo()"
78815
79436
  [canPresent]="slideCount() > 0"
78816
- [menuOpen]="mobileSheet() === 'menu'"
78817
- (toggleMenu)="mobileSheet.set(mobileSheet() === 'menu' ? null : 'menu')"
79437
+ [menuOpen]="mobileSheetSvc.mobileSheet() === 'menu'"
79438
+ (toggleMenu)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'menu' ? null : 'menu')"
78818
79439
  (undo)="editor.undo()"
78819
79440
  (redo)="editor.redo()"
78820
- (save)="saveAsPptx()"
78821
- (present)="present()"
79441
+ (save)="fileIO.saveAsPptx()"
79442
+ (present)="presentationMode.present()"
78822
79443
  />
78823
79444
  }
78824
79445
 
@@ -78845,12 +79466,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78845
79466
  </nav>
78846
79467
  }
78847
79468
 
78848
- <main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
79469
+ <main class="pptx-ng-main" #mainEl (pointermove)="collabCursor.onPointerMove($event)">
78849
79470
  <pptx-slide-canvas
78850
79471
  [slide]="activeSlide()"
78851
79472
  [canvasSize]="loader.canvasSize()"
78852
79473
  [mediaDataUrls]="loader.mediaDataUrls()"
78853
- [zoom]="zoom()"
79474
+ [zoom]="zoomSvc.zoom()"
78854
79475
  [editable]="canEdit()"
78855
79476
  [selectedIds]="editor.selectedIds()"
78856
79477
  [showGrid]="showGrid()"
@@ -78863,32 +79484,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78863
79484
  [drawWidth]="activeDrawWidth()"
78864
79485
  [editTemplateMode]="editor.editTemplateMode()"
78865
79486
  [templateElements]="activeTemplateElements()"
78866
- (elementSelect)="onElementSelect($event)"
78867
- (backgroundClick)="onBackgroundClick()"
79487
+ (elementSelect)="canvasEditing.onElementSelect($event)"
79488
+ (backgroundClick)="canvasEditing.onBackgroundClick()"
78868
79489
  (marqueeSelect)="editor.select($event)"
78869
79490
  (transformStart)="editor.beginTransform($event.label)"
78870
79491
  (transformUpdate)="editor.applyTransform(activeSlideIndex(), $event.id, $event.box)"
78871
79492
  (rotateUpdate)="
78872
79493
  editor.applyTransform(activeSlideIndex(), $event.id, { rotation: $event.rotation })
78873
79494
  "
78874
- (contextMenu)="onContextMenu($event)"
78875
- [editingId]="editingId()"
78876
- (textEditStart)="onTextEditStart($event.id)"
78877
- (textCommit)="onTextCommit($event)"
78878
- (textCancel)="editingId.set(null)"
78879
- (textFormat)="onTextFormat($event)"
78880
- (inkStrokeComplete)="onInkStrokeComplete($event)"
78881
- (eraserHit)="onEraserHit($event)"
78882
- (cellCommit)="onTableCellCommit($event)"
78883
- (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)"
78884
79505
  />
78885
79506
  @if (collab.connected()) {
78886
- <pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
79507
+ <pptx-collaboration-cursors [cursors]="collabCursor.cursors()" [zoom]="zoomSvc.zoom()" />
78887
79508
  <pptx-remote-selection-overlay
78888
79509
  [presences]="collab.presence()"
78889
79510
  [elements]="activeSlide()?.elements ?? []"
78890
79511
  [activeSlideIndex]="activeSlideIndex()"
78891
- [zoom]="zoom()"
79512
+ [zoom]="zoomSvc.zoom()"
78892
79513
  />
78893
79514
  }
78894
79515
  @if (collab.active() && collab.presence().length > 0) {
@@ -78900,11 +79521,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78900
79521
  />
78901
79522
  </div>
78902
79523
  }
78903
- @if (showNotes() && !mobile.isMobile()) {
79524
+ @if (mobileSheetSvc.showNotes() && !mobile.isMobile()) {
78904
79525
  <aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
78905
79526
  <pptx-notes-panel
78906
79527
  [slide]="activeSlide()"
78907
- (update)="onNotesUpdate($event)"
79528
+ (update)="canvasEditing.onNotesUpdate($event)"
78908
79529
  />
78909
79530
  </aside>
78910
79531
  }
@@ -78913,25 +79534,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78913
79534
  <!--
78914
79535
  Single inspector host for every right-rail panel. On mobile it docks
78915
79536
  full-width below the canvas and is swipe-dismissable (the grab handle
78916
- feeds onInspectorPointerDown/Move/Up); a downward swipe past the
78917
- 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.
78918
79540
  -->
78919
- @if (visibleInspectorKind(); as kind) {
79541
+ @if (inspectorPanel.visibleInspectorKind(); as kind) {
78920
79542
  <aside
78921
79543
  class="pptx-ng-inspector-host"
78922
- [attr.aria-label]="inspectorLabel() | translate"
79544
+ [attr.aria-label]="inspectorPanel.inspectorLabel() | translate"
78923
79545
  [style.transform]="
78924
- inspectorDragY() > 0 ? 'translateY(' + inspectorDragY() + 'px)' : null
79546
+ inspectorPanel.inspectorDrag.dragY() > 0 ? 'translateY(' + inspectorPanel.inspectorDrag.dragY() + 'px)' : null
78925
79547
  "
78926
- [style.transition]="inspectorDragging() ? 'none' : 'transform 150ms ease-out'"
79548
+ [style.transition]="inspectorPanel.inspectorDrag.dragging() ? 'none' : 'transform 150ms ease-out'"
78927
79549
  >
78928
79550
  <!-- Swipe-down-to-dismiss grab handle (mobile only; hidden on desktop). -->
78929
79551
  <div
78930
79552
  class="pptx-ng-idrawer-grab"
78931
- (pointerdown)="onInspectorPointerDown($event)"
78932
- (pointermove)="onInspectorPointerMove($event)"
78933
- (pointerup)="onInspectorPointerUp($event)"
78934
- (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)"
78935
79557
  >
78936
79558
  <div class="pptx-ng-idrawer-handle"></div>
78937
79559
  </div>
@@ -78958,9 +79580,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78958
79580
  [elements]="activeSlide()?.elements ?? []"
78959
79581
  [selectedIds]="editor.selectedIds()"
78960
79582
  (selectElement)="editor.select([$event])"
78961
- (bringForward)="onSelectionPaneBringForward($event)"
78962
- (sendBackward)="onSelectionPaneSendBackward($event)"
78963
- (toggleHidden)="onToggleElementHidden($event)"
79583
+ (bringForward)="canvasEditing.onSelectionPaneBringForward($event)"
79584
+ (sendBackward)="canvasEditing.onSelectionPaneSendBackward($event)"
79585
+ (toggleHidden)="canvasEditing.onToggleElementHidden($event)"
78964
79586
  />
78965
79587
  }
78966
79588
  @case ('element') {
@@ -78984,7 +79606,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78984
79606
  <input
78985
79607
  type="color"
78986
79608
  [attr.value]="sl.backgroundColor || '#ffffff'"
78987
- (change)="onSlideBackground($event)"
79609
+ (change)="canvasEditing.onSlideBackground($event)"
78988
79610
  />
78989
79611
  </label>
78990
79612
  <label class="pptx-ng-prop-row pptx-ng-prop-col">
@@ -78992,8 +79614,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
78992
79614
  <textarea
78993
79615
  rows="5"
78994
79616
  [attr.placeholder]="'pptx.viewer.speakerNotesPlaceholder' | translate"
78995
- (change)="onSlideNotes($event)"
78996
- (blur)="onSlideNotes($event)"
79617
+ (change)="canvasEditing.onSlideNotes($event)"
79618
+ (blur)="canvasEditing.onSlideNotes($event)"
78997
79619
  >{{ sl.notes || '' }}</textarea
78998
79620
  >
78999
79621
  </label>
@@ -79012,17 +79634,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79012
79634
  [slideCount]="slideCount()"
79013
79635
  [canEdit]="canEdit()"
79014
79636
  [dirty]="editor.dirty()"
79015
- [notesOpen]="showNotes()"
79016
- [zoomPercent]="zoomPercent()"
79637
+ [notesOpen]="mobileSheetSvc.showNotes()"
79638
+ [zoomPercent]="zoomSvc.zoomPercent()"
79017
79639
  [sorterActive]="showSorter()"
79018
- [presenting]="presenting()"
79019
- (toggleNotes)="toggleNotes()"
79640
+ [presenting]="presentationMode.presenting()"
79641
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
79020
79642
  (normalView)="showSorter.set(false)"
79021
79643
  (openSorter)="showSorter.set(true)"
79022
- (slideShow)="present()"
79023
- (zoomIn)="zoomIn()"
79024
- (zoomOut)="zoomOut()"
79025
- (zoomReset)="zoomReset()"
79644
+ (slideShow)="presentationMode.present()"
79645
+ (zoomIn)="zoomSvc.zoomIn()"
79646
+ (zoomOut)="zoomSvc.zoomOut()"
79647
+ (zoomReset)="zoomSvc.zoomReset()"
79026
79648
  />
79027
79649
  }
79028
79650
  }
@@ -79038,19 +79660,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79038
79660
  />
79039
79661
  }
79040
79662
 
79041
- @if (presenting()) {
79663
+ @if (presentationMode.presenting()) {
79042
79664
  <pptx-presentation-overlay
79043
79665
  [slides]="customShowsCtl.presentationSlides()"
79044
79666
  [canvasSize]="loader.canvasSize()"
79045
79667
  [mediaDataUrls]="loader.mediaDataUrls()"
79046
79668
  [startIndex]="customShowsCtl.presentationStartIndex()"
79047
- (indexChange)="onPresentationIndexChange($event)"
79048
- (annotationsExit)="onPresentationAnnotationsExit($event)"
79049
- (closed)="presenting.set(false)"
79669
+ (indexChange)="presentationMode.onPresentationIndexChange($event)"
79670
+ (annotationsExit)="presentationMode.onPresentationAnnotationsExit($event)"
79671
+ (closed)="presentationMode.presenting.set(false)"
79050
79672
  />
79051
79673
  }
79052
79674
 
79053
- @if (presentingPresenter()) {
79675
+ @if (presentationMode.presentingPresenter()) {
79054
79676
  @if (mobile.isMobile()) {
79055
79677
  <!-- Single-column mobile presenter layout (phones / landscape phones). -->
79056
79678
  <pptx-mobile-presenter-view
@@ -79058,9 +79680,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79058
79680
  [currentSlideIndex]="activeSlideIndex()"
79059
79681
  [canvasSize]="loader.canvasSize()"
79060
79682
  [mediaDataUrls]="loader.mediaDataUrls()"
79061
- [presentationStartTime]="presenterStartTime()"
79683
+ [presentationStartTime]="presentationMode.presenterStartTime()"
79062
79684
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
79063
- (exit)="exitPresenter()"
79685
+ (exit)="presentationMode.exitPresenter()"
79064
79686
  />
79065
79687
  } @else {
79066
79688
  <pptx-presenter-view
@@ -79068,12 +79690,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79068
79690
  [currentSlideIndex]="activeSlideIndex()"
79069
79691
  [canvasSize]="loader.canvasSize()"
79070
79692
  [mediaDataUrls]="loader.mediaDataUrls()"
79071
- [presentationStartTime]="presenterStartTime()"
79693
+ [presentationStartTime]="presentationMode.presenterStartTime()"
79072
79694
  [isAudienceWindowOpen]="presenterWindow.isAudienceWindowOpen()"
79073
79695
  (movePresentationSlide)="goTo(activeSlideIndex() + $event)"
79074
- (openAudienceWindow)="openAudienceWindow()"
79696
+ (openAudienceWindow)="presentationMode.openAudienceWindow()"
79075
79697
  (closeAudienceWindow)="presenterWindow.closeAudienceWindow()"
79076
- (exit)="exitPresenter()"
79698
+ (exit)="presentationMode.exitPresenter()"
79077
79699
  />
79078
79700
  }
79079
79701
  }
@@ -79098,27 +79720,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79098
79720
  />
79099
79721
  }
79100
79722
 
79101
- @if (canEdit() && contextMenuPos(); as m) {
79723
+ @if (canEdit() && canvasEditing.contextMenuPos(); as m) {
79102
79724
  <pptx-editor-context-menu
79103
79725
  [x]="m.x"
79104
79726
  [y]="m.y"
79105
79727
  [slideIndex]="activeSlideIndex()"
79106
- (closed)="contextMenuPos.set(null)"
79728
+ (closed)="canvasEditing.contextMenuPos.set(null)"
79107
79729
  />
79108
79730
  }
79109
79731
 
79110
79732
  <pptx-theme-gallery
79111
- [open]="showThemeGallery()"
79112
- [activeName]="activeThemeName()"
79113
- (applyTheme)="applyThemePreset($event)"
79114
- (close)="showThemeGallery.set(false)"
79733
+ [open]="themeGallery.showThemeGallery()"
79734
+ [activeName]="themeGallery.activeThemeName()"
79735
+ (applyTheme)="themeGallery.applyThemePreset($event)"
79736
+ (close)="themeGallery.showThemeGallery.set(false)"
79115
79737
  />
79116
79738
 
79117
79739
  <pptx-properties-dialog
79118
- [open]="showProperties()"
79119
- [properties]="coreProperties()"
79120
- (save)="onPropertiesSave($event)"
79121
- (close)="showProperties.set(false)"
79740
+ [open]="docProperties.showProperties()"
79741
+ [properties]="docProperties.coreProperties()"
79742
+ (save)="docProperties.onPropertiesSave($event)"
79743
+ (close)="docProperties.showProperties.set(false)"
79122
79744
  />
79123
79745
 
79124
79746
  <!-- Secondary dialogs / side panels (equation, set-up show, password,
@@ -79134,10 +79756,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79134
79756
 
79135
79757
  @if (canEdit()) {
79136
79758
  <pptx-hyperlink-dialog
79137
- [open]="showHyperlink()"
79759
+ [open]="docProperties.showHyperlink()"
79138
79760
  [element]="selectedElement()"
79139
- (save)="onHyperlinkSave($event)"
79140
- (close)="showHyperlink.set(false)"
79761
+ (save)="docProperties.onHyperlinkSave($event)"
79762
+ (close)="docProperties.showHyperlink.set(false)"
79141
79763
  />
79142
79764
  }
79143
79765
 
@@ -79208,29 +79830,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79208
79830
  <!-- ── Mobile chrome (narrow / touch viewports only) ─────────────── -->
79209
79831
  @if (mobile.isMobile() && !loader.loading() && !loader.error()) {
79210
79832
  <pptx-mobile-slides-sheet
79211
- [open]="mobileSheet() === 'slides'"
79833
+ [open]="mobileSheetSvc.mobileSheet() === 'slides'"
79212
79834
  [slides]="displaySlidesMut()"
79213
79835
  [canvasSize]="loader.canvasSize()"
79214
79836
  [mediaDataUrls]="loader.mediaDataUrls()"
79215
79837
  [activeIndex]="activeSlideIndex()"
79216
79838
  (jumpToSlide)="goTo($event)"
79217
- (closed)="mobileSheet.set(null)"
79839
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
79218
79840
  />
79219
79841
 
79220
79842
  <pptx-mobile-menu-sheet
79221
- [open]="mobileSheet() === 'menu'"
79843
+ [open]="mobileSheetSvc.mobileSheet() === 'menu'"
79222
79844
  [slideCount]="slideCount()"
79223
79845
  [exporting]="xport.exporting()"
79224
- [showNotes]="showNotes()"
79846
+ [showNotes]="mobileSheetSvc.showNotes()"
79225
79847
  [canEdit]="canEdit()"
79226
- (closed)="mobileSheet.set(null)"
79848
+ (closed)="mobileSheetSvc.mobileSheet.set(null)"
79227
79849
  (openFind)="findReplace.showFind.set(true)"
79228
79850
  (openSorter)="showSorter.set(true)"
79229
- (toggleNotes)="toggleNotes()"
79230
- (insertText)="onMobileInsert()"
79231
- (present)="present()"
79232
- (openFile)="openFile()"
79233
- (savePptx)="saveAsPptx()"
79851
+ (toggleNotes)="mobileSheetSvc.toggleNotes()"
79852
+ (insertText)="mobileSheetSvc.onMobileInsert()"
79853
+ (present)="presentationMode.present()"
79854
+ (openFile)="fileIO.openFile()"
79855
+ (savePptx)="fileIO.saveAsPptx()"
79234
79856
  (exportPng)="xport.exportPng()"
79235
79857
  (exportPdf)="xport.exportPdf()"
79236
79858
  (exportGif)="xport.exportGif()"
@@ -79246,24 +79868,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79246
79868
  the visual viewport ends up below the document on mobile (100vh layout
79247
79869
  viewport < dynamic viewport), leaving its textarea unreachable to taps.
79248
79870
  Mirrors React, where the notes panel is a flow sibling below the canvas. -->
79249
- @if (showNotes()) {
79871
+ @if (mobileSheetSvc.showNotes()) {
79250
79872
  <div
79251
79873
  class="pptx-ng-mobile-notes-sheet"
79252
- [style.transform]="notesDragY() > 0 ? 'translateY(' + notesDragY() + 'px)' : null"
79253
- [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'"
79254
79876
  >
79255
79877
  <!-- Swipe-down-to-dismiss grab handle (kept in-flow so the keyboard
79256
79878
  can't push the textarea out of reach). -->
79257
79879
  <div
79258
79880
  class="pptx-ng-mnotes-grab"
79259
- (pointerdown)="onNotesPointerDown($event)"
79260
- (pointermove)="onNotesPointerMove($event)"
79261
- (pointerup)="onNotesPointerUp($event)"
79262
- (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)"
79263
79885
  >
79264
79886
  <div class="pptx-ng-mnotes-handle"></div>
79265
79887
  </div>
79266
- <pptx-notes-panel [slide]="activeSlide()" (update)="onNotesUpdate($event)" />
79888
+ <pptx-notes-panel [slide]="activeSlide()" (update)="canvasEditing.onNotesUpdate($event)" />
79267
79889
  </div>
79268
79890
  }
79269
79891
 
@@ -79277,11 +79899,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
79277
79899
  [slideCount]="slideCount()"
79278
79900
  [commentCount]="activeComments().length"
79279
79901
  [activeSheet]="mobileBarSheet()"
79280
- (openSlides)="mobileSheet.set(mobileSheet() === 'slides' ? null : 'slides')"
79281
- (insert)="onMobileInsert()"
79902
+ (openSlides)="mobileSheetSvc.mobileSheet.set(mobileSheetSvc.mobileSheet() === 'slides' ? null : 'slides')"
79903
+ (insert)="mobileSheetSvc.onMobileInsert()"
79282
79904
  (openFormat)="onMobileFormat()"
79283
- (openComments)="togglePanel('comments')"
79284
- (notes)="toggleNotes()"
79905
+ (openComments)="inspectorPanel.togglePanel('comments')"
79906
+ (notes)="mobileSheetSvc.toggleNotes()"
79285
79907
  />
79286
79908
  }
79287
79909
  </div>