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