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