pptx-angular-viewer 0.1.0
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/LICENSE +202 -0
- package/NOTICE +18 -0
- package/README.md +211 -0
- package/fesm2022/pptx-angular-viewer.mjs +1358 -0
- package/fesm2022/pptx-angular-viewer.mjs.map +1 -0
- package/package.json +49 -0
- package/pptx-angular-viewer.css +201 -0
- package/types/pptx-angular-viewer.d.ts +406 -0
|
@@ -0,0 +1,1358 @@
|
|
|
1
|
+
import { NgStyle, NgClass } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, signal, computed, inject, DestroyRef, Injectable, input, ChangeDetectionStrategy, Component, output, effect } from '@angular/core';
|
|
4
|
+
import { guideEmuToPx, PptxHandler, EncryptedFileError, hasShapeProperties, hasTextProperties } from 'pptx-viewer-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default dark-theme color values.
|
|
8
|
+
*
|
|
9
|
+
* These correspond to the built-in dark UI of the PowerPoint viewer and
|
|
10
|
+
* use Tailwind's gray palette as the neutral scale with indigo as the
|
|
11
|
+
* primary accent.
|
|
12
|
+
*/
|
|
13
|
+
const defaultThemeColors = {
|
|
14
|
+
background: '#030712', // gray-950
|
|
15
|
+
foreground: '#f3f4f6', // gray-100
|
|
16
|
+
card: '#111827', // gray-900
|
|
17
|
+
cardForeground: '#f3f4f6', // gray-100
|
|
18
|
+
popover: '#111827', // gray-900
|
|
19
|
+
popoverForeground: '#f3f4f6', // gray-100
|
|
20
|
+
primary: '#6366f1', // indigo-500
|
|
21
|
+
primaryForeground: '#ffffff', // white
|
|
22
|
+
secondary: '#1f2937', // gray-800
|
|
23
|
+
secondaryForeground: '#f3f4f6', // gray-100
|
|
24
|
+
muted: '#1f2937', // gray-800
|
|
25
|
+
mutedForeground: '#9ca3af', // gray-400
|
|
26
|
+
accent: '#1f2937', // gray-800
|
|
27
|
+
accentForeground: '#f3f4f6', // gray-100
|
|
28
|
+
destructive: '#ef4444', // red-500
|
|
29
|
+
destructiveForeground: '#ffffff', // white
|
|
30
|
+
border: '#374151', // gray-700
|
|
31
|
+
input: '#374151', // gray-700
|
|
32
|
+
ring: '#6366f1', // indigo-500
|
|
33
|
+
};
|
|
34
|
+
/** Default border-radius. */
|
|
35
|
+
const defaultRadius = '0.5rem';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map from camelCase ViewerThemeColors keys to kebab-case CSS custom
|
|
39
|
+
* property suffixes (the part after `--pptx-`).
|
|
40
|
+
*/
|
|
41
|
+
const COLOR_KEY_TO_CSS = {
|
|
42
|
+
background: 'background',
|
|
43
|
+
foreground: 'foreground',
|
|
44
|
+
card: 'card',
|
|
45
|
+
cardForeground: 'card-foreground',
|
|
46
|
+
popover: 'popover',
|
|
47
|
+
popoverForeground: 'popover-foreground',
|
|
48
|
+
primary: 'primary',
|
|
49
|
+
primaryForeground: 'primary-foreground',
|
|
50
|
+
secondary: 'secondary',
|
|
51
|
+
secondaryForeground: 'secondary-foreground',
|
|
52
|
+
muted: 'muted',
|
|
53
|
+
mutedForeground: 'muted-foreground',
|
|
54
|
+
accent: 'accent',
|
|
55
|
+
accentForeground: 'accent-foreground',
|
|
56
|
+
destructive: 'destructive',
|
|
57
|
+
destructiveForeground: 'destructive-foreground',
|
|
58
|
+
border: 'border',
|
|
59
|
+
input: 'input',
|
|
60
|
+
ring: 'ring',
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Convert a `ViewerTheme` into a flat `Record<string, string>` of CSS
|
|
64
|
+
* custom properties (including the `--` prefix) ready to be spread onto
|
|
65
|
+
* a `style` attribute.
|
|
66
|
+
*
|
|
67
|
+
* Only properties that differ from the built-in defaults are emitted when
|
|
68
|
+
* `omitDefaults` is true (the default).
|
|
69
|
+
*/
|
|
70
|
+
function themeToCssVars(theme, omitDefaults = false) {
|
|
71
|
+
const vars = {};
|
|
72
|
+
if (!theme) {
|
|
73
|
+
return vars;
|
|
74
|
+
}
|
|
75
|
+
// ── Colors ───────────────────────────────────────────────────────
|
|
76
|
+
const colors = theme.colors;
|
|
77
|
+
if (colors) {
|
|
78
|
+
for (const [key, cssSuffix] of Object.entries(COLOR_KEY_TO_CSS)) {
|
|
79
|
+
const value = colors[key];
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (omitDefaults && value === defaultThemeColors[key]) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
vars[`--pptx-${cssSuffix}`] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ── Radius ───────────────────────────────────────────────────────
|
|
90
|
+
if (theme.radius !== undefined) {
|
|
91
|
+
if (!omitDefaults || theme.radius !== defaultRadius) {
|
|
92
|
+
vars['--pptx-radius'] = theme.radius;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Escape-hatch custom properties ───────────────────────────────
|
|
96
|
+
if (theme.cssVars) {
|
|
97
|
+
for (const [key, value] of Object.entries(theme.cssVars)) {
|
|
98
|
+
vars[key] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return vars;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the complete set of CSS custom properties with all defaults.
|
|
105
|
+
* Useful for generating a full fallback stylesheet.
|
|
106
|
+
*/
|
|
107
|
+
function defaultCssVars() {
|
|
108
|
+
const vars = {};
|
|
109
|
+
for (const [key, cssSuffix] of Object.entries(COLOR_KEY_TO_CSS)) {
|
|
110
|
+
vars[`--pptx-${cssSuffix}`] = defaultThemeColors[key];
|
|
111
|
+
}
|
|
112
|
+
vars['--pptx-radius'] = defaultRadius;
|
|
113
|
+
return vars;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Recursively walks an element tree and pushes every media element
|
|
118
|
+
* into the supplied collector array.
|
|
119
|
+
*/
|
|
120
|
+
function collectMediaElements(elements, collector) {
|
|
121
|
+
for (const element of elements) {
|
|
122
|
+
if (element.type === 'media') {
|
|
123
|
+
collector.push(element);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (element.type === 'group' && element.children?.length) {
|
|
127
|
+
collectMediaElements(element.children, collector);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Collect all unique image archive paths across all slides that need
|
|
133
|
+
* to be resolved to displayable URLs (Blob URLs).
|
|
134
|
+
*
|
|
135
|
+
* This covers:
|
|
136
|
+
* - Picture elements (`imageData`, `svgData`)
|
|
137
|
+
* - Media poster frames (`posterFrameData`)
|
|
138
|
+
*
|
|
139
|
+
* Returns the set of unique archive paths, plus a list of element/field
|
|
140
|
+
* references that need to be updated once each path resolves.
|
|
141
|
+
*/
|
|
142
|
+
function collectImagePaths(slides) {
|
|
143
|
+
const paths = new Set();
|
|
144
|
+
const refs = [];
|
|
145
|
+
const walkElements = (elements) => {
|
|
146
|
+
for (const el of elements) {
|
|
147
|
+
if (el.type === 'picture' || el.type === 'image') {
|
|
148
|
+
const pic = el;
|
|
149
|
+
if (pic.imagePath && !pic.imageData && !isExternalUrl(pic.imagePath)) {
|
|
150
|
+
paths.add(pic.imagePath);
|
|
151
|
+
refs.push({ element: el, field: 'imageData', path: pic.imagePath });
|
|
152
|
+
}
|
|
153
|
+
if (pic.svgPath && !pic.svgData && !isExternalUrl(pic.svgPath)) {
|
|
154
|
+
paths.add(pic.svgPath);
|
|
155
|
+
refs.push({ element: el, field: 'svgData', path: pic.svgPath });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (el.type === 'media') {
|
|
159
|
+
const media = el;
|
|
160
|
+
if (media.posterFramePath &&
|
|
161
|
+
!media.posterFrameData &&
|
|
162
|
+
!isExternalUrl(media.posterFramePath)) {
|
|
163
|
+
paths.add(media.posterFramePath);
|
|
164
|
+
refs.push({
|
|
165
|
+
element: el,
|
|
166
|
+
field: 'posterFrameData',
|
|
167
|
+
path: media.posterFramePath,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (el.type === 'group' && el.children?.length) {
|
|
172
|
+
walkElements(el.children);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
for (const slide of slides) {
|
|
177
|
+
walkElements(slide.elements);
|
|
178
|
+
}
|
|
179
|
+
return { paths, refs };
|
|
180
|
+
}
|
|
181
|
+
function isExternalUrl(path) {
|
|
182
|
+
return (path.startsWith('http://') ||
|
|
183
|
+
path.startsWith('https://') ||
|
|
184
|
+
path.startsWith('data:') ||
|
|
185
|
+
path.startsWith('blob:'));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Converts raw EMU-based drawing guides from the parsed presentation
|
|
189
|
+
* and the first slide into pixel-based `GuideEntry` objects.
|
|
190
|
+
*/
|
|
191
|
+
function buildInitialGuides(presentationGuides, firstSlideGuides) {
|
|
192
|
+
const guides = [];
|
|
193
|
+
if (presentationGuides) {
|
|
194
|
+
for (const g of presentationGuides) {
|
|
195
|
+
guides.push({
|
|
196
|
+
id: g.id,
|
|
197
|
+
axis: g.orientation === 'horz' ? 'h' : 'v',
|
|
198
|
+
position: guideEmuToPx(g.positionEmu),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (firstSlideGuides) {
|
|
203
|
+
for (const g of firstSlideGuides) {
|
|
204
|
+
guides.push({
|
|
205
|
+
id: g.id,
|
|
206
|
+
axis: g.orientation === 'horz' ? 'h' : 'v',
|
|
207
|
+
position: guideEmuToPx(g.positionEmu),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return guides;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Framework-agnostic public types shared by the viewer bindings.
|
|
216
|
+
*
|
|
217
|
+
* These were duplicated in the React (`types-ui.ts`) and Vue (`viewer/types.ts`)
|
|
218
|
+
* packages; this is the canonical copy. Each binding layers its own
|
|
219
|
+
* framework-specific prop/event/handle types on top of these.
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Scalar viewer defaults shared by the UI bindings.
|
|
224
|
+
*
|
|
225
|
+
* Subset of the React package's `constants/scalar.ts` that the Vue and Angular
|
|
226
|
+
* viewers also need. Additional constant groups (toolbar presets, shape styles,
|
|
227
|
+
* transitions, etc.) remain per-binding until those features are ported.
|
|
228
|
+
*/
|
|
229
|
+
/** Default slide canvas width in pixels when the file declares none. */
|
|
230
|
+
const DEFAULT_CANVAS_WIDTH = 1280;
|
|
231
|
+
/** Default slide canvas height in pixels when the file declares none. */
|
|
232
|
+
const DEFAULT_CANVAS_HEIGHT = 720;
|
|
233
|
+
/** Fallback text colour. */
|
|
234
|
+
const DEFAULT_TEXT_COLOR = '#111827';
|
|
235
|
+
/** Fallback shape fill colour. */
|
|
236
|
+
const DEFAULT_FILL_COLOR = '#3b82f6';
|
|
237
|
+
/** Fallback shape stroke colour. */
|
|
238
|
+
const DEFAULT_STROKE_COLOR = '#1f2937';
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* pptx-viewer-shared — framework-agnostic viewer logic shared by the
|
|
242
|
+
* React (`pptx-viewer`), Vue (`pptx-vue-viewer`), and Angular
|
|
243
|
+
* (`pptx-angular-viewer`) bindings.
|
|
244
|
+
*
|
|
245
|
+
* Everything exported here is pure TypeScript (no framework imports), so each
|
|
246
|
+
* UI binding consumes one copy instead of duplicating it.
|
|
247
|
+
*
|
|
248
|
+
* Current surface:
|
|
249
|
+
* - theme: ViewerTheme types, default palette, CSS-variable helpers.
|
|
250
|
+
* - loader: load-pipeline helpers (media/image collection, guides).
|
|
251
|
+
* - types: CanvasSize, CollaborationConfig, CollaborationRole.
|
|
252
|
+
* - constants: scalar viewer defaults (canvas size, fallback colours).
|
|
253
|
+
*
|
|
254
|
+
* Roadmap (see packages/angular/PORTING.md and packages/vue/PORTING.md):
|
|
255
|
+
* color resolution, geometry/clip-paths, connector routing, animation
|
|
256
|
+
* timeline engine, table-merge math, morph matching, export data helpers.
|
|
257
|
+
*/
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Internal re-export of `pptx-viewer-shared`.
|
|
261
|
+
*
|
|
262
|
+
* `pptx-viewer-shared` is an INTERNAL, non-published package (`"private": true`).
|
|
263
|
+
* Its source is vendored into `./shared-src` at build time by
|
|
264
|
+
* `scripts/inline-shared.mjs` (a generated, git-ignored directory), so the
|
|
265
|
+
* shared code compiles as part of THIS library and ships **inlined** in the
|
|
266
|
+
* published FESM. As a result `pptx-viewer-shared` never appears in the
|
|
267
|
+
* published `package.json`.
|
|
268
|
+
*
|
|
269
|
+
* All Angular sources import shared symbols from THIS barrel, never from the
|
|
270
|
+
* bare `'pptx-viewer-shared'` specifier (which ng-packagr would externalize).
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Theme system for the Angular PowerPoint viewer.
|
|
275
|
+
*
|
|
276
|
+
* Angular counterpart of the React `ViewerThemeProvider` / `useViewerTheme`
|
|
277
|
+
* context and the Vue `provide`/`inject` theme provider. The `ViewerTheme`
|
|
278
|
+
* type, default palette, and `themeToCssVars` helper are framework-agnostic
|
|
279
|
+
* and live in `pptx-viewer-shared`.
|
|
280
|
+
*/
|
|
281
|
+
/** DI token carrying the active `ViewerTheme` (or `undefined`). */
|
|
282
|
+
const VIEWER_THEME = new InjectionToken('PPTX_VIEWER_THEME');
|
|
283
|
+
/**
|
|
284
|
+
* Provide a `ViewerTheme` to a subtree.
|
|
285
|
+
*
|
|
286
|
+
* Typically you do **not** need this — passing a `theme` input to
|
|
287
|
+
* `<pptx-viewer>` is sufficient. Use this to share a theme across multiple
|
|
288
|
+
* viewers or a wider component subtree.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* bootstrapApplication(AppComponent, {
|
|
293
|
+
* providers: [provideViewerTheme({ colors: { primary: '#6366f1' } })],
|
|
294
|
+
* });
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
function provideViewerTheme(theme) {
|
|
298
|
+
return { provide: VIEWER_THEME, useValue: theme };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Build an `[ngStyle]`-compatible map of CSS custom properties for a theme.
|
|
302
|
+
* Returns an empty object when the theme contributes no variables.
|
|
303
|
+
*/
|
|
304
|
+
function themeStyle(theme) {
|
|
305
|
+
if (!theme) {
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
return themeToCssVars(theme);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* `LoadContentService` — Angular port of the React `useLoadContent` hook and
|
|
313
|
+
* the Vue `useLoadContent` composable.
|
|
314
|
+
*
|
|
315
|
+
* Parses `.pptx` bytes into reactive signals via the framework-agnostic
|
|
316
|
+
* `PptxHandler` from `pptx-viewer-core`. All heavy lifting (ZIP, XML parse,
|
|
317
|
+
* theme/master/layout resolution, media extraction) lives in core and the
|
|
318
|
+
* pure helpers live in `pptx-viewer-shared`; this service only wires the async
|
|
319
|
+
* load into Angular signals and manages Blob-URL / handler lifecycle.
|
|
320
|
+
*
|
|
321
|
+
* Provide it at the component level so its lifetime tracks the host viewer:
|
|
322
|
+
* `@Component({ providers: [LoadContentService] })`.
|
|
323
|
+
*
|
|
324
|
+
* This is the viewer-first subset; the React hook also populated ~25 extra
|
|
325
|
+
* pieces of presentation metadata (sections, custom shows, embedded fonts,
|
|
326
|
+
* digital signatures, …). Those are tracked in PORTING.md and added here as
|
|
327
|
+
* the corresponding features are ported.
|
|
328
|
+
*/
|
|
329
|
+
class LoadContentService {
|
|
330
|
+
/** Parsed slides (with image Blob URLs patched in). */
|
|
331
|
+
slides = signal([], /* @ts-ignore */
|
|
332
|
+
...(ngDevMode ? [{ debugName: "slides" }] : /* istanbul ignore next */ []));
|
|
333
|
+
/** Slide canvas size in pixels. */
|
|
334
|
+
canvasSize = signal({
|
|
335
|
+
width: DEFAULT_CANVAS_WIDTH,
|
|
336
|
+
height: DEFAULT_CANVAS_HEIGHT,
|
|
337
|
+
}, /* @ts-ignore */
|
|
338
|
+
...(ngDevMode ? [{ debugName: "canvasSize" }] : /* istanbul ignore next */ []));
|
|
339
|
+
/** Resolved presentation theme. */
|
|
340
|
+
theme = signal(undefined, /* @ts-ignore */
|
|
341
|
+
...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
342
|
+
/** Slide masters (for placeholder/background resolution). */
|
|
343
|
+
slideMasters = signal([], /* @ts-ignore */
|
|
344
|
+
...(ngDevMode ? [{ debugName: "slideMasters" }] : /* istanbul ignore next */ []));
|
|
345
|
+
/** Archive-path → displayable URL map for media + poster frames. */
|
|
346
|
+
mediaDataUrls = signal(new Map(), /* @ts-ignore */
|
|
347
|
+
...(ngDevMode ? [{ debugName: "mediaDataUrls" }] : /* istanbul ignore next */ []));
|
|
348
|
+
/** True while a load is in flight. */
|
|
349
|
+
loading = signal(false, /* @ts-ignore */
|
|
350
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
351
|
+
/** Error message from the last failed load, or null. */
|
|
352
|
+
error = signal(null, /* @ts-ignore */
|
|
353
|
+
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
354
|
+
/** True when the file is password-protected and could not be opened. */
|
|
355
|
+
isEncrypted = signal(false, /* @ts-ignore */
|
|
356
|
+
...(ngDevMode ? [{ debugName: "isEncrypted" }] : /* istanbul ignore next */ []));
|
|
357
|
+
/** Number of loaded slides. */
|
|
358
|
+
slideCount = computed(() => this.slides().length, /* @ts-ignore */
|
|
359
|
+
...(ngDevMode ? [{ debugName: "slideCount" }] : /* istanbul ignore next */ []));
|
|
360
|
+
handler = null;
|
|
361
|
+
renderToken = 0;
|
|
362
|
+
activeBlobUrls = [];
|
|
363
|
+
constructor() {
|
|
364
|
+
inject(DestroyRef).onDestroy(() => {
|
|
365
|
+
this.renderToken++;
|
|
366
|
+
this.revokeBlobUrls(this.activeBlobUrls);
|
|
367
|
+
this.revokeBlobUrls(Array.from(this.mediaDataUrls().values()));
|
|
368
|
+
this.disposeHandler();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/** Serialise the current presentation back to `.pptx` bytes. */
|
|
372
|
+
async getContent() {
|
|
373
|
+
if (!this.handler) {
|
|
374
|
+
throw new Error('No presentation is loaded.');
|
|
375
|
+
}
|
|
376
|
+
return this.handler.save(this.slides());
|
|
377
|
+
}
|
|
378
|
+
/** Parse the supplied `.pptx` bytes into the reactive signals. */
|
|
379
|
+
async load(raw) {
|
|
380
|
+
if (!raw) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const token = ++this.renderToken;
|
|
384
|
+
const loadBlobUrls = [];
|
|
385
|
+
try {
|
|
386
|
+
this.loading.set(true);
|
|
387
|
+
this.error.set(null);
|
|
388
|
+
this.isEncrypted.set(false);
|
|
389
|
+
const buffer = raw instanceof Uint8Array
|
|
390
|
+
? raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength)
|
|
391
|
+
: raw;
|
|
392
|
+
const fileSizeMB = buffer instanceof ArrayBuffer ? buffer.byteLength / (1024 * 1024) : 0;
|
|
393
|
+
if (fileSizeMB > 50) {
|
|
394
|
+
console.warn(`[pptx] Large file detected (${fileSizeMB.toFixed(1)} MB). ` +
|
|
395
|
+
`Loading may use significant memory.`);
|
|
396
|
+
}
|
|
397
|
+
const previousHandler = this.handler;
|
|
398
|
+
const newHandler = new PptxHandler();
|
|
399
|
+
const parsed = await newHandler.load(buffer);
|
|
400
|
+
if (token !== this.renderToken) {
|
|
401
|
+
newHandler.dispose();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
previousHandler?.dispose();
|
|
405
|
+
// ── Resolve media Blob URLs (audio/video + poster frames) ──
|
|
406
|
+
const mediaElements = [];
|
|
407
|
+
for (const slide of parsed.slides) {
|
|
408
|
+
collectMediaElements(slide.elements, mediaElements);
|
|
409
|
+
}
|
|
410
|
+
this.revokeBlobUrls(Array.from(this.mediaDataUrls().values()));
|
|
411
|
+
const nextMediaUrls = new Map();
|
|
412
|
+
await Promise.all(mediaElements.map(async (mediaElement) => {
|
|
413
|
+
const mediaPath = mediaElement.mediaPath;
|
|
414
|
+
if (!mediaPath) {
|
|
415
|
+
mediaElement.mediaMissing = true;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const isAudioVideo = mediaElement.mediaType === 'audio' || mediaElement.mediaType === 'video';
|
|
420
|
+
if (isAudioVideo) {
|
|
421
|
+
const arrayBuffer = await newHandler.getMediaArrayBuffer(mediaPath);
|
|
422
|
+
if (arrayBuffer) {
|
|
423
|
+
const mimeType = mediaElement.mediaMimeType || 'application/octet-stream';
|
|
424
|
+
const blob = new Blob([arrayBuffer], { type: mimeType });
|
|
425
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
426
|
+
loadBlobUrls.push(blobUrl);
|
|
427
|
+
nextMediaUrls.set(mediaPath, blobUrl);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
mediaElement.mediaMissing = true;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
const dataUrl = await newHandler.getImageData(mediaPath);
|
|
435
|
+
if (dataUrl) {
|
|
436
|
+
nextMediaUrls.set(mediaPath, dataUrl);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
mediaElement.mediaMissing = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
mediaElement.mediaMissing = true;
|
|
445
|
+
}
|
|
446
|
+
}));
|
|
447
|
+
// ── Resolve lazily-loaded picture Blob URLs ──
|
|
448
|
+
const { paths: imagePaths, refs: imageRefs } = collectImagePaths(parsed.slides);
|
|
449
|
+
let nextSlides = parsed.slides;
|
|
450
|
+
if (imagePaths.size > 0) {
|
|
451
|
+
const resolvedMap = new Map();
|
|
452
|
+
await Promise.all(Array.from(imagePaths).map(async (path) => {
|
|
453
|
+
try {
|
|
454
|
+
const url = await newHandler.getImageData(path);
|
|
455
|
+
if (url) {
|
|
456
|
+
resolvedMap.set(path, url);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Non-critical: image will show as broken.
|
|
461
|
+
}
|
|
462
|
+
}));
|
|
463
|
+
const elementPatches = new Map();
|
|
464
|
+
for (const refEntry of imageRefs) {
|
|
465
|
+
const url = resolvedMap.get(refEntry.path);
|
|
466
|
+
if (!url) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const id = refEntry.element.id;
|
|
470
|
+
const existing = elementPatches.get(id) ?? {};
|
|
471
|
+
existing[refEntry.field] = url;
|
|
472
|
+
elementPatches.set(id, existing);
|
|
473
|
+
}
|
|
474
|
+
if (elementPatches.size > 0) {
|
|
475
|
+
const patchElements = (elements) => {
|
|
476
|
+
let mutated = false;
|
|
477
|
+
const next = elements.map((el) => {
|
|
478
|
+
let updated = el;
|
|
479
|
+
const patch = elementPatches.get(el.id);
|
|
480
|
+
if (patch) {
|
|
481
|
+
updated = { ...el, ...patch };
|
|
482
|
+
}
|
|
483
|
+
if (updated.type === 'group' && updated.children?.length) {
|
|
484
|
+
const newChildren = patchElements(updated.children);
|
|
485
|
+
if (newChildren !== updated.children) {
|
|
486
|
+
updated = { ...updated, children: newChildren };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (updated !== el) {
|
|
490
|
+
mutated = true;
|
|
491
|
+
}
|
|
492
|
+
return updated;
|
|
493
|
+
});
|
|
494
|
+
return mutated ? next : elements;
|
|
495
|
+
};
|
|
496
|
+
nextSlides = parsed.slides.map((s) => {
|
|
497
|
+
const newElements = patchElements(s.elements);
|
|
498
|
+
return newElements === s.elements ? s : { ...s, elements: newElements };
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Commit reactive state.
|
|
503
|
+
this.revokeBlobUrls(this.activeBlobUrls);
|
|
504
|
+
this.activeBlobUrls = loadBlobUrls;
|
|
505
|
+
this.handler = newHandler;
|
|
506
|
+
this.slides.set(nextSlides);
|
|
507
|
+
this.mediaDataUrls.set(nextMediaUrls);
|
|
508
|
+
this.canvasSize.set({
|
|
509
|
+
width: parsed.width ?? DEFAULT_CANVAS_WIDTH,
|
|
510
|
+
height: parsed.height ?? DEFAULT_CANVAS_HEIGHT,
|
|
511
|
+
});
|
|
512
|
+
this.theme.set(parsed.theme);
|
|
513
|
+
this.slideMasters.set(parsed.slideMasters ?? []);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
if (token === this.renderToken) {
|
|
517
|
+
if (err instanceof EncryptedFileError) {
|
|
518
|
+
this.isEncrypted.set(true);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
this.error.set(err instanceof Error ? err.message : String(err));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
if (token === this.renderToken) {
|
|
527
|
+
this.loading.set(false);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
disposeHandler() {
|
|
532
|
+
if (this.handler) {
|
|
533
|
+
this.handler.dispose();
|
|
534
|
+
this.handler = null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
revokeBlobUrls(urls) {
|
|
538
|
+
for (const url of urls) {
|
|
539
|
+
if (url.startsWith('blob:')) {
|
|
540
|
+
URL.revokeObjectURL(url);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: LoadContentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
545
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: LoadContentService });
|
|
546
|
+
}
|
|
547
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: LoadContentService, decorators: [{
|
|
548
|
+
type: Injectable
|
|
549
|
+
}], ctorParameters: () => [] });
|
|
550
|
+
|
|
551
|
+
/** Map a number to a CSS pixel string. */
|
|
552
|
+
const px = (n) => `${n}px`;
|
|
553
|
+
/**
|
|
554
|
+
* Absolute container style: position, size, rotation, flip, opacity, z-index.
|
|
555
|
+
* Mirrors the essentials of the React `getContainerStyle`.
|
|
556
|
+
*/
|
|
557
|
+
function getContainerStyle(el, zIndex) {
|
|
558
|
+
const transforms = [];
|
|
559
|
+
if (el.rotation) {
|
|
560
|
+
transforms.push(`rotate(${el.rotation}deg)`);
|
|
561
|
+
}
|
|
562
|
+
if (el.flipHorizontal) {
|
|
563
|
+
transforms.push('scaleX(-1)');
|
|
564
|
+
}
|
|
565
|
+
if (el.flipVertical) {
|
|
566
|
+
transforms.push('scaleY(-1)');
|
|
567
|
+
}
|
|
568
|
+
const style = {
|
|
569
|
+
position: 'absolute',
|
|
570
|
+
left: px(el.x),
|
|
571
|
+
top: px(el.y),
|
|
572
|
+
width: px(el.width),
|
|
573
|
+
height: px(el.height),
|
|
574
|
+
'z-index': zIndex,
|
|
575
|
+
'box-sizing': 'border-box',
|
|
576
|
+
};
|
|
577
|
+
if (transforms.length > 0) {
|
|
578
|
+
style['transform'] = transforms.join(' ');
|
|
579
|
+
}
|
|
580
|
+
if (typeof el.opacity === 'number') {
|
|
581
|
+
style['opacity'] = el.opacity;
|
|
582
|
+
}
|
|
583
|
+
if (el.hidden) {
|
|
584
|
+
style['display'] = 'none';
|
|
585
|
+
}
|
|
586
|
+
return style;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Fill / stroke / corner-radius for shape-like elements. Returns an empty
|
|
590
|
+
* object when the element carries no shape styling.
|
|
591
|
+
*/
|
|
592
|
+
function getShapeFillStrokeStyle(el) {
|
|
593
|
+
if (!hasShapeProperties(el)) {
|
|
594
|
+
return {};
|
|
595
|
+
}
|
|
596
|
+
const ss = el.shapeStyle;
|
|
597
|
+
const style = {};
|
|
598
|
+
if (ss) {
|
|
599
|
+
// Fill — solid only for now (gradients/patterns/images: TODO).
|
|
600
|
+
if (ss.fillColor && ss.fillColor !== 'transparent' && ss.fillMode !== 'none') {
|
|
601
|
+
style['background-color'] = ss.fillColor;
|
|
602
|
+
}
|
|
603
|
+
// Stroke.
|
|
604
|
+
const strokeWidth = Math.max(0, ss.strokeWidth ?? 0);
|
|
605
|
+
if (strokeWidth > 0) {
|
|
606
|
+
const dash = ss.strokeDash && ss.strokeDash !== 'solid'
|
|
607
|
+
? ss.strokeDash === 'dot' || ss.strokeDash === 'sysDot'
|
|
608
|
+
? 'dotted'
|
|
609
|
+
: 'dashed'
|
|
610
|
+
: 'solid';
|
|
611
|
+
style['border'] = `${px(strokeWidth)} ${dash} ${ss.strokeColor ?? DEFAULT_STROKE_COLOR}`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Corner radius — approximate common preset geometries.
|
|
615
|
+
const shapeType = 'shapeType' in el ? el.shapeType : undefined;
|
|
616
|
+
if (shapeType === 'ellipse' || shapeType === 'circle') {
|
|
617
|
+
style['border-radius'] = '50%';
|
|
618
|
+
}
|
|
619
|
+
else if (shapeType === 'roundRect') {
|
|
620
|
+
style['border-radius'] = px(Math.min(el.width, el.height) * 0.1);
|
|
621
|
+
}
|
|
622
|
+
return style;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Text block style for elements that carry text. Mirrors the essentials of the
|
|
626
|
+
* React `getTextStyleForElement`.
|
|
627
|
+
*/
|
|
628
|
+
function getTextBlockStyle(el) {
|
|
629
|
+
if (!hasTextProperties(el)) {
|
|
630
|
+
return {};
|
|
631
|
+
}
|
|
632
|
+
const ts = el.textStyle;
|
|
633
|
+
const style = {
|
|
634
|
+
display: 'flex',
|
|
635
|
+
'flex-direction': 'column',
|
|
636
|
+
width: '100%',
|
|
637
|
+
height: '100%',
|
|
638
|
+
overflow: 'hidden',
|
|
639
|
+
'white-space': 'pre-wrap',
|
|
640
|
+
'word-break': 'break-word',
|
|
641
|
+
};
|
|
642
|
+
if (!ts) {
|
|
643
|
+
style['color'] = DEFAULT_TEXT_COLOR;
|
|
644
|
+
return style;
|
|
645
|
+
}
|
|
646
|
+
style['color'] = ts.color ?? DEFAULT_TEXT_COLOR;
|
|
647
|
+
if (ts.fontFamily) {
|
|
648
|
+
style['font-family'] = ts.fontFamily;
|
|
649
|
+
}
|
|
650
|
+
if (typeof ts.fontSize === 'number') {
|
|
651
|
+
style['font-size'] = `${ts.fontSize}pt`;
|
|
652
|
+
}
|
|
653
|
+
if (ts.bold) {
|
|
654
|
+
style['font-weight'] = 'bold';
|
|
655
|
+
}
|
|
656
|
+
if (ts.italic) {
|
|
657
|
+
style['font-style'] = 'italic';
|
|
658
|
+
}
|
|
659
|
+
const decorations = [];
|
|
660
|
+
if (ts.underline) {
|
|
661
|
+
decorations.push('underline');
|
|
662
|
+
}
|
|
663
|
+
if (ts.strikethrough) {
|
|
664
|
+
decorations.push('line-through');
|
|
665
|
+
}
|
|
666
|
+
if (decorations.length > 0) {
|
|
667
|
+
style['text-decoration'] = decorations.join(' ');
|
|
668
|
+
}
|
|
669
|
+
switch (ts.align) {
|
|
670
|
+
case 'center':
|
|
671
|
+
style['text-align'] = 'center';
|
|
672
|
+
break;
|
|
673
|
+
case 'right':
|
|
674
|
+
style['text-align'] = 'right';
|
|
675
|
+
break;
|
|
676
|
+
case 'justify':
|
|
677
|
+
style['text-align'] = 'justify';
|
|
678
|
+
break;
|
|
679
|
+
default:
|
|
680
|
+
style['text-align'] = 'left';
|
|
681
|
+
}
|
|
682
|
+
switch (ts.vAlign) {
|
|
683
|
+
case 'middle':
|
|
684
|
+
style['justify-content'] = 'center';
|
|
685
|
+
break;
|
|
686
|
+
case 'bottom':
|
|
687
|
+
style['justify-content'] = 'flex-end';
|
|
688
|
+
break;
|
|
689
|
+
default:
|
|
690
|
+
style['justify-content'] = 'flex-start';
|
|
691
|
+
}
|
|
692
|
+
return style;
|
|
693
|
+
}
|
|
694
|
+
/** Resolve a displayable image source for picture/image/media poster frames. */
|
|
695
|
+
function getImageSrc(el, mediaDataUrls) {
|
|
696
|
+
if (el.type === 'picture' || el.type === 'image') {
|
|
697
|
+
return el.imageData ?? (el.imagePath ? mediaDataUrls.get(el.imagePath) : undefined);
|
|
698
|
+
}
|
|
699
|
+
if (el.type === 'media') {
|
|
700
|
+
return (el.posterFrameData ?? (el.posterFramePath ? mediaDataUrls.get(el.posterFramePath) : undefined));
|
|
701
|
+
}
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* ElementRendererComponent — Angular port of the React `ElementRenderer.tsx`
|
|
707
|
+
* and the Vue `ElementRenderer.vue`.
|
|
708
|
+
*
|
|
709
|
+
* Renders a single slide element by its `type` discriminant (viewer-first
|
|
710
|
+
* subset):
|
|
711
|
+
* - `text` / `shape` → positioned box with fill/stroke + rich text
|
|
712
|
+
* - `picture` / `image` → `<img>`
|
|
713
|
+
* - `media` → poster frame (`<img>`) — playback TODO
|
|
714
|
+
* - `group` → recursive children (self-referencing selector)
|
|
715
|
+
* - everything else → labelled placeholder (TODO, see PORTING.md)
|
|
716
|
+
*
|
|
717
|
+
* Interaction (selection, resize, inline editing), connectors, charts, tables,
|
|
718
|
+
* SmartArt, ink, OLE, and 3D are not yet ported.
|
|
719
|
+
*/
|
|
720
|
+
class ElementRendererComponent {
|
|
721
|
+
element = input.required(/* @ts-ignore */
|
|
722
|
+
...(ngDevMode ? [{ debugName: "element" }] : /* istanbul ignore next */ []));
|
|
723
|
+
mediaDataUrls = input(new Map(), /* @ts-ignore */
|
|
724
|
+
...(ngDevMode ? [{ debugName: "mediaDataUrls" }] : /* istanbul ignore next */ []));
|
|
725
|
+
zIndex = input(0, /* @ts-ignore */
|
|
726
|
+
...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
|
|
727
|
+
containerStyle = computed(() => getContainerStyle(this.element(), this.zIndex()), /* @ts-ignore */
|
|
728
|
+
...(ngDevMode ? [{ debugName: "containerStyle" }] : /* istanbul ignore next */ []));
|
|
729
|
+
shapeContainerStyle = computed(() => ({
|
|
730
|
+
...this.containerStyle(),
|
|
731
|
+
...getShapeFillStrokeStyle(this.element()),
|
|
732
|
+
}), /* @ts-ignore */
|
|
733
|
+
...(ngDevMode ? [{ debugName: "shapeContainerStyle" }] : /* istanbul ignore next */ []));
|
|
734
|
+
textStyle = computed(() => getTextBlockStyle(this.element()), /* @ts-ignore */
|
|
735
|
+
...(ngDevMode ? [{ debugName: "textStyle" }] : /* istanbul ignore next */ []));
|
|
736
|
+
imageSrc = computed(() => getImageSrc(this.element(), this.mediaDataUrls()), /* @ts-ignore */
|
|
737
|
+
...(ngDevMode ? [{ debugName: "imageSrc" }] : /* istanbul ignore next */ []));
|
|
738
|
+
children = computed(() => {
|
|
739
|
+
const el = this.element();
|
|
740
|
+
return el.type === 'group' ? (el.children ?? []) : [];
|
|
741
|
+
}, /* @ts-ignore */
|
|
742
|
+
...(ngDevMode ? [{ debugName: "children" }] : /* istanbul ignore next */ []));
|
|
743
|
+
isShapeLike = computed(() => this.element().type === 'text' || this.element().type === 'shape', /* @ts-ignore */
|
|
744
|
+
...(ngDevMode ? [{ debugName: "isShapeLike" }] : /* istanbul ignore next */ []));
|
|
745
|
+
isImageLike = computed(() => this.element().type === 'picture' || this.element().type === 'image', /* @ts-ignore */
|
|
746
|
+
...(ngDevMode ? [{ debugName: "isImageLike" }] : /* istanbul ignore next */ []));
|
|
747
|
+
paragraphs = computed(() => {
|
|
748
|
+
const el = this.element();
|
|
749
|
+
if (!hasTextProperties(el)) {
|
|
750
|
+
return [];
|
|
751
|
+
}
|
|
752
|
+
const segments = el.textSegments;
|
|
753
|
+
if (!segments || segments.length === 0) {
|
|
754
|
+
return el.text ? [[{ text: el.text, style: {} }]] : [];
|
|
755
|
+
}
|
|
756
|
+
const out = [[]];
|
|
757
|
+
for (const seg of segments) {
|
|
758
|
+
if (seg.isParagraphBreak) {
|
|
759
|
+
out.push([]);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const current = out[out.length - 1];
|
|
763
|
+
const text = seg.isLineBreak ? '\n' : seg.text;
|
|
764
|
+
if (text) {
|
|
765
|
+
current.push({ text, style: this.segmentStyle(seg) });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return out.filter((p) => p.length > 0 || out.length === 1);
|
|
769
|
+
}, /* @ts-ignore */
|
|
770
|
+
...(ngDevMode ? [{ debugName: "paragraphs" }] : /* istanbul ignore next */ []));
|
|
771
|
+
hasText = computed(() => this.paragraphs().some((p) => p.length > 0), /* @ts-ignore */
|
|
772
|
+
...(ngDevMode ? [{ debugName: "hasText" }] : /* istanbul ignore next */ []));
|
|
773
|
+
placeholderLabel = computed(() => {
|
|
774
|
+
const map = {
|
|
775
|
+
table: 'Table',
|
|
776
|
+
chart: 'Chart',
|
|
777
|
+
smartArt: 'SmartArt',
|
|
778
|
+
connector: 'Connector',
|
|
779
|
+
group: 'Group',
|
|
780
|
+
media: 'Media',
|
|
781
|
+
ink: 'Ink',
|
|
782
|
+
ole: 'Embedded object',
|
|
783
|
+
model3d: '3D model',
|
|
784
|
+
zoom: 'Zoom',
|
|
785
|
+
};
|
|
786
|
+
return map[this.element().type] ?? this.element().type;
|
|
787
|
+
}, /* @ts-ignore */
|
|
788
|
+
...(ngDevMode ? [{ debugName: "placeholderLabel" }] : /* istanbul ignore next */ []));
|
|
789
|
+
segmentStyle(seg) {
|
|
790
|
+
const s = seg.style ?? {};
|
|
791
|
+
const style = {};
|
|
792
|
+
if (s.fontFamily) {
|
|
793
|
+
style['font-family'] = s.fontFamily;
|
|
794
|
+
}
|
|
795
|
+
if (typeof s.fontSize === 'number') {
|
|
796
|
+
style['font-size'] = `${s.fontSize}pt`;
|
|
797
|
+
}
|
|
798
|
+
if (s.color) {
|
|
799
|
+
style['color'] = s.color;
|
|
800
|
+
}
|
|
801
|
+
if (s.bold) {
|
|
802
|
+
style['font-weight'] = 'bold';
|
|
803
|
+
}
|
|
804
|
+
if (s.italic) {
|
|
805
|
+
style['font-style'] = 'italic';
|
|
806
|
+
}
|
|
807
|
+
const deco = [];
|
|
808
|
+
if (s.underline) {
|
|
809
|
+
deco.push('underline');
|
|
810
|
+
}
|
|
811
|
+
if (s.strikethrough) {
|
|
812
|
+
deco.push('line-through');
|
|
813
|
+
}
|
|
814
|
+
if (deco.length > 0) {
|
|
815
|
+
style['text-decoration'] = deco.join(' ');
|
|
816
|
+
}
|
|
817
|
+
return style;
|
|
818
|
+
}
|
|
819
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ElementRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
820
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
821
|
+
@switch (true) {
|
|
822
|
+
@case (element().type === 'group') {
|
|
823
|
+
<div
|
|
824
|
+
class="pptx-ng-element pptx-ng-group"
|
|
825
|
+
[ngStyle]="containerStyle()"
|
|
826
|
+
[attr.data-element-id]="element().id"
|
|
827
|
+
>
|
|
828
|
+
@for (child of children(); track child.id) {
|
|
829
|
+
<pptx-element-renderer
|
|
830
|
+
[element]="child"
|
|
831
|
+
[mediaDataUrls]="mediaDataUrls()"
|
|
832
|
+
[zIndex]="$index"
|
|
833
|
+
/>
|
|
834
|
+
}
|
|
835
|
+
</div>
|
|
836
|
+
}
|
|
837
|
+
@case (isImageLike()) {
|
|
838
|
+
<div
|
|
839
|
+
class="pptx-ng-element pptx-ng-image"
|
|
840
|
+
[ngStyle]="containerStyle()"
|
|
841
|
+
[attr.data-element-id]="element().id"
|
|
842
|
+
>
|
|
843
|
+
@if (imageSrc()) {
|
|
844
|
+
<img [src]="imageSrc()" alt="" class="pptx-ng-img" />
|
|
845
|
+
}
|
|
846
|
+
</div>
|
|
847
|
+
}
|
|
848
|
+
@case (element().type === 'media') {
|
|
849
|
+
<div
|
|
850
|
+
class="pptx-ng-element pptx-ng-media"
|
|
851
|
+
[ngStyle]="containerStyle()"
|
|
852
|
+
[attr.data-element-id]="element().id"
|
|
853
|
+
>
|
|
854
|
+
@if (imageSrc()) {
|
|
855
|
+
<img [src]="imageSrc()" alt="" class="pptx-ng-img" />
|
|
856
|
+
} @else {
|
|
857
|
+
<div class="pptx-ng-placeholder">{{ placeholderLabel() }}</div>
|
|
858
|
+
}
|
|
859
|
+
</div>
|
|
860
|
+
}
|
|
861
|
+
@case (isShapeLike()) {
|
|
862
|
+
<div
|
|
863
|
+
class="pptx-ng-element pptx-ng-shape"
|
|
864
|
+
[ngStyle]="shapeContainerStyle()"
|
|
865
|
+
[attr.data-element-id]="element().id"
|
|
866
|
+
>
|
|
867
|
+
@if (hasText()) {
|
|
868
|
+
<div class="pptx-ng-text" [ngStyle]="textStyle()">
|
|
869
|
+
@for (para of paragraphs(); track $index) {
|
|
870
|
+
<p class="pptx-ng-para">
|
|
871
|
+
@for (run of para; track $index) {
|
|
872
|
+
@if (
|
|
873
|
+
run.text ===
|
|
874
|
+
'
|
|
875
|
+
'
|
|
876
|
+
) {
|
|
877
|
+
<br />
|
|
878
|
+
} @else {
|
|
879
|
+
<span [ngStyle]="run.style">{{ run.text }}</span>
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
</p>
|
|
883
|
+
}
|
|
884
|
+
</div>
|
|
885
|
+
}
|
|
886
|
+
</div>
|
|
887
|
+
}
|
|
888
|
+
@default {
|
|
889
|
+
<div
|
|
890
|
+
class="pptx-ng-element pptx-ng-unsupported"
|
|
891
|
+
[ngStyle]="containerStyle()"
|
|
892
|
+
[attr.data-element-id]="element().id"
|
|
893
|
+
>
|
|
894
|
+
<div class="pptx-ng-placeholder">{{ placeholderLabel() }}</div>
|
|
895
|
+
</div>
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
899
|
+
}
|
|
900
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ElementRendererComponent, decorators: [{
|
|
901
|
+
type: Component,
|
|
902
|
+
args: [{
|
|
903
|
+
selector: 'pptx-element-renderer',
|
|
904
|
+
standalone: true,
|
|
905
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
906
|
+
imports: [NgStyle],
|
|
907
|
+
template: `
|
|
908
|
+
@switch (true) {
|
|
909
|
+
@case (element().type === 'group') {
|
|
910
|
+
<div
|
|
911
|
+
class="pptx-ng-element pptx-ng-group"
|
|
912
|
+
[ngStyle]="containerStyle()"
|
|
913
|
+
[attr.data-element-id]="element().id"
|
|
914
|
+
>
|
|
915
|
+
@for (child of children(); track child.id) {
|
|
916
|
+
<pptx-element-renderer
|
|
917
|
+
[element]="child"
|
|
918
|
+
[mediaDataUrls]="mediaDataUrls()"
|
|
919
|
+
[zIndex]="$index"
|
|
920
|
+
/>
|
|
921
|
+
}
|
|
922
|
+
</div>
|
|
923
|
+
}
|
|
924
|
+
@case (isImageLike()) {
|
|
925
|
+
<div
|
|
926
|
+
class="pptx-ng-element pptx-ng-image"
|
|
927
|
+
[ngStyle]="containerStyle()"
|
|
928
|
+
[attr.data-element-id]="element().id"
|
|
929
|
+
>
|
|
930
|
+
@if (imageSrc()) {
|
|
931
|
+
<img [src]="imageSrc()" alt="" class="pptx-ng-img" />
|
|
932
|
+
}
|
|
933
|
+
</div>
|
|
934
|
+
}
|
|
935
|
+
@case (element().type === 'media') {
|
|
936
|
+
<div
|
|
937
|
+
class="pptx-ng-element pptx-ng-media"
|
|
938
|
+
[ngStyle]="containerStyle()"
|
|
939
|
+
[attr.data-element-id]="element().id"
|
|
940
|
+
>
|
|
941
|
+
@if (imageSrc()) {
|
|
942
|
+
<img [src]="imageSrc()" alt="" class="pptx-ng-img" />
|
|
943
|
+
} @else {
|
|
944
|
+
<div class="pptx-ng-placeholder">{{ placeholderLabel() }}</div>
|
|
945
|
+
}
|
|
946
|
+
</div>
|
|
947
|
+
}
|
|
948
|
+
@case (isShapeLike()) {
|
|
949
|
+
<div
|
|
950
|
+
class="pptx-ng-element pptx-ng-shape"
|
|
951
|
+
[ngStyle]="shapeContainerStyle()"
|
|
952
|
+
[attr.data-element-id]="element().id"
|
|
953
|
+
>
|
|
954
|
+
@if (hasText()) {
|
|
955
|
+
<div class="pptx-ng-text" [ngStyle]="textStyle()">
|
|
956
|
+
@for (para of paragraphs(); track $index) {
|
|
957
|
+
<p class="pptx-ng-para">
|
|
958
|
+
@for (run of para; track $index) {
|
|
959
|
+
@if (
|
|
960
|
+
run.text ===
|
|
961
|
+
'
|
|
962
|
+
'
|
|
963
|
+
) {
|
|
964
|
+
<br />
|
|
965
|
+
} @else {
|
|
966
|
+
<span [ngStyle]="run.style">{{ run.text }}</span>
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
</p>
|
|
970
|
+
}
|
|
971
|
+
</div>
|
|
972
|
+
}
|
|
973
|
+
</div>
|
|
974
|
+
}
|
|
975
|
+
@default {
|
|
976
|
+
<div
|
|
977
|
+
class="pptx-ng-element pptx-ng-unsupported"
|
|
978
|
+
[ngStyle]="containerStyle()"
|
|
979
|
+
[attr.data-element-id]="element().id"
|
|
980
|
+
>
|
|
981
|
+
<div class="pptx-ng-placeholder">{{ placeholderLabel() }}</div>
|
|
982
|
+
</div>
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
`,
|
|
986
|
+
}]
|
|
987
|
+
}], 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 }] }] } });
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* SlideCanvasComponent — Angular port of the React `SlideCanvas.tsx` and Vue
|
|
991
|
+
* `SlideCanvas.vue` (viewer-first subset).
|
|
992
|
+
*
|
|
993
|
+
* Renders the active slide as a fixed-size stage scaled by `zoom`, with each
|
|
994
|
+
* element absolutely positioned. The React version additionally layered in
|
|
995
|
+
* rulers, grid, guides, marquee/selection, connector-creation, drawing, and
|
|
996
|
+
* collaboration overlays — all tracked in PORTING.md.
|
|
997
|
+
*/
|
|
998
|
+
class SlideCanvasComponent {
|
|
999
|
+
slide = input(undefined, /* @ts-ignore */
|
|
1000
|
+
...(ngDevMode ? [{ debugName: "slide" }] : /* istanbul ignore next */ []));
|
|
1001
|
+
canvasSize = input.required(/* @ts-ignore */
|
|
1002
|
+
...(ngDevMode ? [{ debugName: "canvasSize" }] : /* istanbul ignore next */ []));
|
|
1003
|
+
mediaDataUrls = input(new Map(), /* @ts-ignore */
|
|
1004
|
+
...(ngDevMode ? [{ debugName: "mediaDataUrls" }] : /* istanbul ignore next */ []));
|
|
1005
|
+
zoom = input(1, /* @ts-ignore */
|
|
1006
|
+
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
1007
|
+
elements = computed(() => this.slide()?.elements ?? [], /* @ts-ignore */
|
|
1008
|
+
...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
|
|
1009
|
+
wrapperStyle = computed(() => {
|
|
1010
|
+
const scale = this.zoom();
|
|
1011
|
+
const size = this.canvasSize();
|
|
1012
|
+
return {
|
|
1013
|
+
width: `${size.width * scale}px`,
|
|
1014
|
+
height: `${size.height * scale}px`,
|
|
1015
|
+
position: 'relative',
|
|
1016
|
+
margin: '1rem auto',
|
|
1017
|
+
};
|
|
1018
|
+
}, /* @ts-ignore */
|
|
1019
|
+
...(ngDevMode ? [{ debugName: "wrapperStyle" }] : /* istanbul ignore next */ []));
|
|
1020
|
+
stageStyle = computed(() => {
|
|
1021
|
+
const scale = this.zoom();
|
|
1022
|
+
const size = this.canvasSize();
|
|
1023
|
+
const slide = this.slide();
|
|
1024
|
+
const style = {
|
|
1025
|
+
width: `${size.width}px`,
|
|
1026
|
+
height: `${size.height}px`,
|
|
1027
|
+
transform: `scale(${scale})`,
|
|
1028
|
+
'transform-origin': 'top left',
|
|
1029
|
+
position: 'relative',
|
|
1030
|
+
overflow: 'hidden',
|
|
1031
|
+
'background-color': slide?.backgroundColor && slide.backgroundColor !== 'transparent'
|
|
1032
|
+
? slide.backgroundColor
|
|
1033
|
+
: '#ffffff',
|
|
1034
|
+
'background-size': '100% 100%',
|
|
1035
|
+
'box-shadow': '0 10px 40px rgba(0, 0, 0, 0.35)',
|
|
1036
|
+
};
|
|
1037
|
+
if (slide?.backgroundImage) {
|
|
1038
|
+
style['background-image'] = `url(${slide.backgroundImage})`;
|
|
1039
|
+
}
|
|
1040
|
+
return style;
|
|
1041
|
+
}, /* @ts-ignore */
|
|
1042
|
+
...(ngDevMode ? [{ debugName: "stageStyle" }] : /* istanbul ignore next */ []));
|
|
1043
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SlideCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1044
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", 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 } }, ngImport: i0, template: `
|
|
1045
|
+
<div class="pptx-ng-canvas-viewport">
|
|
1046
|
+
<div class="pptx-ng-canvas-wrapper" [ngStyle]="wrapperStyle()">
|
|
1047
|
+
<div
|
|
1048
|
+
class="pptx-ng-canvas-stage"
|
|
1049
|
+
role="region"
|
|
1050
|
+
aria-roledescription="slide"
|
|
1051
|
+
[ngStyle]="stageStyle()"
|
|
1052
|
+
>
|
|
1053
|
+
@for (element of elements(); track element.id; let i = $index) {
|
|
1054
|
+
<pptx-element-renderer
|
|
1055
|
+
[element]="element"
|
|
1056
|
+
[mediaDataUrls]="mediaDataUrls()"
|
|
1057
|
+
[zIndex]="i"
|
|
1058
|
+
/>
|
|
1059
|
+
}
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: ElementRendererComponent, selector: "pptx-element-renderer", inputs: ["element", "mediaDataUrls", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1064
|
+
}
|
|
1065
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: SlideCanvasComponent, decorators: [{
|
|
1066
|
+
type: Component,
|
|
1067
|
+
args: [{
|
|
1068
|
+
selector: 'pptx-slide-canvas',
|
|
1069
|
+
standalone: true,
|
|
1070
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1071
|
+
imports: [NgStyle, ElementRendererComponent],
|
|
1072
|
+
template: `
|
|
1073
|
+
<div class="pptx-ng-canvas-viewport">
|
|
1074
|
+
<div class="pptx-ng-canvas-wrapper" [ngStyle]="wrapperStyle()">
|
|
1075
|
+
<div
|
|
1076
|
+
class="pptx-ng-canvas-stage"
|
|
1077
|
+
role="region"
|
|
1078
|
+
aria-roledescription="slide"
|
|
1079
|
+
[ngStyle]="stageStyle()"
|
|
1080
|
+
>
|
|
1081
|
+
@for (element of elements(); track element.id; let i = $index) {
|
|
1082
|
+
<pptx-element-renderer
|
|
1083
|
+
[element]="element"
|
|
1084
|
+
[mediaDataUrls]="mediaDataUrls()"
|
|
1085
|
+
[zIndex]="i"
|
|
1086
|
+
/>
|
|
1087
|
+
}
|
|
1088
|
+
</div>
|
|
1089
|
+
</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
`,
|
|
1092
|
+
}]
|
|
1093
|
+
}], propDecorators: { slide: [{ type: i0.Input, args: [{ isSignal: true, alias: "slide", required: false }] }], canvasSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasSize", required: true }] }], mediaDataUrls: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaDataUrls", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
|
|
1094
|
+
|
|
1095
|
+
const ZOOM_STEP = 0.1;
|
|
1096
|
+
const ZOOM_MIN = 0.2;
|
|
1097
|
+
const ZOOM_MAX = 3;
|
|
1098
|
+
/**
|
|
1099
|
+
* PowerPointViewerComponent — Angular port of the React `PowerPointViewer.tsx`
|
|
1100
|
+
* and Vue `PowerPointViewer.vue`.
|
|
1101
|
+
*
|
|
1102
|
+
* Top-level orchestrator that loads `.pptx` bytes and renders the slides with
|
|
1103
|
+
* navigation and zoom. This is the viewer-first milestone of the port: the
|
|
1104
|
+
* React component additionally composes a full editor (toolbar, inspector
|
|
1105
|
+
* panels, dialogs, presentation mode, collaboration, export). The roadmap and
|
|
1106
|
+
* per-area status live in `packages/angular/PORTING.md`.
|
|
1107
|
+
*
|
|
1108
|
+
* Conventions vs. React/Vue:
|
|
1109
|
+
* - React `forwardRef` handle / Vue `defineExpose` → public {@link getContent}
|
|
1110
|
+
* method (reach it via a template ref or `viewChild`).
|
|
1111
|
+
* - React callback props / Vue emits → Angular `output()` events.
|
|
1112
|
+
* - React theme context / Vue provide-inject → `themeStyle` CSS vars applied to
|
|
1113
|
+
* the root element (app-wide sharing via `provideViewerTheme`).
|
|
1114
|
+
*/
|
|
1115
|
+
class PowerPointViewerComponent {
|
|
1116
|
+
/** PowerPoint content as Uint8Array (or ArrayBuffer). */
|
|
1117
|
+
content = input(null, /* @ts-ignore */
|
|
1118
|
+
...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
|
|
1119
|
+
/** Whether editing actions are enabled. (Editor chrome not yet ported.) */
|
|
1120
|
+
canEdit = input(false, /* @ts-ignore */
|
|
1121
|
+
...(ngDevMode ? [{ debugName: "canEdit" }] : /* istanbul ignore next */ []));
|
|
1122
|
+
/** Optional class applied to the root element. */
|
|
1123
|
+
class = input('', /* @ts-ignore */
|
|
1124
|
+
...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
|
|
1125
|
+
/** Theme configuration for customising the viewer's appearance. */
|
|
1126
|
+
theme = input(undefined, /* @ts-ignore */
|
|
1127
|
+
...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
|
|
1128
|
+
/** Optional real-time collaboration config (accepted for API parity; not yet implemented). */
|
|
1129
|
+
collaboration = input(undefined, /* @ts-ignore */
|
|
1130
|
+
...(ngDevMode ? [{ debugName: "collaboration" }] : /* istanbul ignore next */ []));
|
|
1131
|
+
/** Fired when the active slide changes. */
|
|
1132
|
+
activeSlideChange = output();
|
|
1133
|
+
/** Fired when the unsaved-changes flag toggles. (Editing not yet ported.) */
|
|
1134
|
+
dirtyChange = output();
|
|
1135
|
+
/** Fired when the in-memory content changes after edits. (Editing not yet ported.) */
|
|
1136
|
+
contentChange = output();
|
|
1137
|
+
loader = inject(LoadContentService);
|
|
1138
|
+
activeSlideIndex = signal(0, /* @ts-ignore */
|
|
1139
|
+
...(ngDevMode ? [{ debugName: "activeSlideIndex" }] : /* istanbul ignore next */ []));
|
|
1140
|
+
slideCount = this.loader.slideCount;
|
|
1141
|
+
activeSlide = computed(() => this.loader.slides()[this.activeSlideIndex()], /* @ts-ignore */
|
|
1142
|
+
...(ngDevMode ? [{ debugName: "activeSlide" }] : /* istanbul ignore next */ []));
|
|
1143
|
+
rootStyle = computed(() => themeStyle(this.theme()), /* @ts-ignore */
|
|
1144
|
+
...(ngDevMode ? [{ debugName: "rootStyle" }] : /* istanbul ignore next */ []));
|
|
1145
|
+
zoom = signal(1, /* @ts-ignore */
|
|
1146
|
+
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
1147
|
+
zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
|
|
1148
|
+
...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
|
|
1149
|
+
constructor() {
|
|
1150
|
+
// Load whenever the `content` input changes.
|
|
1151
|
+
effect(() => {
|
|
1152
|
+
const content = this.content();
|
|
1153
|
+
void this.loader.load(content);
|
|
1154
|
+
});
|
|
1155
|
+
// Reset to the first slide whenever a new presentation finishes loading.
|
|
1156
|
+
effect(() => {
|
|
1157
|
+
// Read slides to track; reset index out of band.
|
|
1158
|
+
this.loader.slides();
|
|
1159
|
+
this.activeSlideIndex.set(0);
|
|
1160
|
+
});
|
|
1161
|
+
// Emit navigation changes.
|
|
1162
|
+
effect(() => {
|
|
1163
|
+
this.activeSlideChange.emit(this.activeSlideIndex());
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
/** Serialise the current presentation to `.pptx` bytes (imperative handle). */
|
|
1167
|
+
getContent() {
|
|
1168
|
+
return this.loader.getContent();
|
|
1169
|
+
}
|
|
1170
|
+
goTo(index) {
|
|
1171
|
+
if (index < 0 || index >= this.slideCount()) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
this.activeSlideIndex.set(index);
|
|
1175
|
+
}
|
|
1176
|
+
goPrev() {
|
|
1177
|
+
this.goTo(this.activeSlideIndex() - 1);
|
|
1178
|
+
}
|
|
1179
|
+
goNext() {
|
|
1180
|
+
this.goTo(this.activeSlideIndex() + 1);
|
|
1181
|
+
}
|
|
1182
|
+
zoomIn() {
|
|
1183
|
+
this.zoom.set(Math.min(ZOOM_MAX, Number((this.zoom() + ZOOM_STEP).toFixed(2))));
|
|
1184
|
+
}
|
|
1185
|
+
zoomOut() {
|
|
1186
|
+
this.zoom.set(Math.max(ZOOM_MIN, Number((this.zoom() - ZOOM_STEP).toFixed(2))));
|
|
1187
|
+
}
|
|
1188
|
+
zoomReset() {
|
|
1189
|
+
this.zoom.set(1);
|
|
1190
|
+
}
|
|
1191
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PowerPointViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1192
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PowerPointViewerComponent, isStandalone: true, selector: "pptx-viewer", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, canEdit: { classPropertyName: "canEdit", publicName: "canEdit", isSignal: true, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, collaboration: { classPropertyName: "collaboration", publicName: "collaboration", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeSlideChange: "activeSlideChange", dirtyChange: "dirtyChange", contentChange: "contentChange" }, providers: [LoadContentService], ngImport: i0, template: `
|
|
1193
|
+
<div class="pptx-ng-viewer" [ngClass]="class()" [ngStyle]="rootStyle()">
|
|
1194
|
+
@if (loader.loading()) {
|
|
1195
|
+
<div class="pptx-ng-state pptx-ng-loading">
|
|
1196
|
+
<div class="pptx-ng-spinner" aria-hidden="true"></div>
|
|
1197
|
+
<p>Loading presentation…</p>
|
|
1198
|
+
</div>
|
|
1199
|
+
} @else if (loader.isEncrypted()) {
|
|
1200
|
+
<div class="pptx-ng-state pptx-ng-error">
|
|
1201
|
+
<p>This presentation is password-protected and cannot be opened.</p>
|
|
1202
|
+
</div>
|
|
1203
|
+
} @else if (loader.error()) {
|
|
1204
|
+
<div class="pptx-ng-state pptx-ng-error">
|
|
1205
|
+
<p>Failed to load presentation.</p>
|
|
1206
|
+
<pre class="pptx-ng-error-detail">{{ loader.error() }}</pre>
|
|
1207
|
+
</div>
|
|
1208
|
+
} @else {
|
|
1209
|
+
<header class="pptx-ng-toolbar">
|
|
1210
|
+
<div class="pptx-ng-nav">
|
|
1211
|
+
<button type="button" [disabled]="activeSlideIndex() <= 0" (click)="goPrev()">‹</button>
|
|
1212
|
+
<span class="pptx-ng-slide-counter">
|
|
1213
|
+
{{ slideCount() === 0 ? 0 : activeSlideIndex() + 1 }} / {{ slideCount() }}
|
|
1214
|
+
</span>
|
|
1215
|
+
<button
|
|
1216
|
+
type="button"
|
|
1217
|
+
[disabled]="activeSlideIndex() >= slideCount() - 1"
|
|
1218
|
+
(click)="goNext()"
|
|
1219
|
+
>
|
|
1220
|
+
›
|
|
1221
|
+
</button>
|
|
1222
|
+
</div>
|
|
1223
|
+
<div class="pptx-ng-zoom">
|
|
1224
|
+
<button type="button" (click)="zoomOut()">−</button>
|
|
1225
|
+
<button type="button" class="pptx-ng-zoom-value" (click)="zoomReset()">
|
|
1226
|
+
{{ zoomPercent() }}%
|
|
1227
|
+
</button>
|
|
1228
|
+
<button type="button" (click)="zoomIn()">+</button>
|
|
1229
|
+
</div>
|
|
1230
|
+
</header>
|
|
1231
|
+
|
|
1232
|
+
<div class="pptx-ng-body">
|
|
1233
|
+
<nav class="pptx-ng-thumbnails" aria-label="Slides">
|
|
1234
|
+
@for (slide of loader.slides(); track slide.id; let i = $index) {
|
|
1235
|
+
<button
|
|
1236
|
+
type="button"
|
|
1237
|
+
class="pptx-ng-thumb"
|
|
1238
|
+
[class.is-active]="i === activeSlideIndex()"
|
|
1239
|
+
(click)="goTo(i)"
|
|
1240
|
+
>
|
|
1241
|
+
<span class="pptx-ng-thumb-index">{{ i + 1 }}</span>
|
|
1242
|
+
</button>
|
|
1243
|
+
}
|
|
1244
|
+
</nav>
|
|
1245
|
+
|
|
1246
|
+
<main class="pptx-ng-main">
|
|
1247
|
+
<pptx-slide-canvas
|
|
1248
|
+
[slide]="activeSlide()"
|
|
1249
|
+
[canvasSize]="loader.canvasSize()"
|
|
1250
|
+
[mediaDataUrls]="loader.mediaDataUrls()"
|
|
1251
|
+
[zoom]="zoom()"
|
|
1252
|
+
/>
|
|
1253
|
+
</main>
|
|
1254
|
+
</div>
|
|
1255
|
+
}
|
|
1256
|
+
</div>
|
|
1257
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1258
|
+
}
|
|
1259
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PowerPointViewerComponent, decorators: [{
|
|
1260
|
+
type: Component,
|
|
1261
|
+
args: [{
|
|
1262
|
+
selector: 'pptx-viewer',
|
|
1263
|
+
standalone: true,
|
|
1264
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1265
|
+
providers: [LoadContentService],
|
|
1266
|
+
imports: [NgClass, NgStyle, SlideCanvasComponent],
|
|
1267
|
+
template: `
|
|
1268
|
+
<div class="pptx-ng-viewer" [ngClass]="class()" [ngStyle]="rootStyle()">
|
|
1269
|
+
@if (loader.loading()) {
|
|
1270
|
+
<div class="pptx-ng-state pptx-ng-loading">
|
|
1271
|
+
<div class="pptx-ng-spinner" aria-hidden="true"></div>
|
|
1272
|
+
<p>Loading presentation…</p>
|
|
1273
|
+
</div>
|
|
1274
|
+
} @else if (loader.isEncrypted()) {
|
|
1275
|
+
<div class="pptx-ng-state pptx-ng-error">
|
|
1276
|
+
<p>This presentation is password-protected and cannot be opened.</p>
|
|
1277
|
+
</div>
|
|
1278
|
+
} @else if (loader.error()) {
|
|
1279
|
+
<div class="pptx-ng-state pptx-ng-error">
|
|
1280
|
+
<p>Failed to load presentation.</p>
|
|
1281
|
+
<pre class="pptx-ng-error-detail">{{ loader.error() }}</pre>
|
|
1282
|
+
</div>
|
|
1283
|
+
} @else {
|
|
1284
|
+
<header class="pptx-ng-toolbar">
|
|
1285
|
+
<div class="pptx-ng-nav">
|
|
1286
|
+
<button type="button" [disabled]="activeSlideIndex() <= 0" (click)="goPrev()">‹</button>
|
|
1287
|
+
<span class="pptx-ng-slide-counter">
|
|
1288
|
+
{{ slideCount() === 0 ? 0 : activeSlideIndex() + 1 }} / {{ slideCount() }}
|
|
1289
|
+
</span>
|
|
1290
|
+
<button
|
|
1291
|
+
type="button"
|
|
1292
|
+
[disabled]="activeSlideIndex() >= slideCount() - 1"
|
|
1293
|
+
(click)="goNext()"
|
|
1294
|
+
>
|
|
1295
|
+
›
|
|
1296
|
+
</button>
|
|
1297
|
+
</div>
|
|
1298
|
+
<div class="pptx-ng-zoom">
|
|
1299
|
+
<button type="button" (click)="zoomOut()">−</button>
|
|
1300
|
+
<button type="button" class="pptx-ng-zoom-value" (click)="zoomReset()">
|
|
1301
|
+
{{ zoomPercent() }}%
|
|
1302
|
+
</button>
|
|
1303
|
+
<button type="button" (click)="zoomIn()">+</button>
|
|
1304
|
+
</div>
|
|
1305
|
+
</header>
|
|
1306
|
+
|
|
1307
|
+
<div class="pptx-ng-body">
|
|
1308
|
+
<nav class="pptx-ng-thumbnails" aria-label="Slides">
|
|
1309
|
+
@for (slide of loader.slides(); track slide.id; let i = $index) {
|
|
1310
|
+
<button
|
|
1311
|
+
type="button"
|
|
1312
|
+
class="pptx-ng-thumb"
|
|
1313
|
+
[class.is-active]="i === activeSlideIndex()"
|
|
1314
|
+
(click)="goTo(i)"
|
|
1315
|
+
>
|
|
1316
|
+
<span class="pptx-ng-thumb-index">{{ i + 1 }}</span>
|
|
1317
|
+
</button>
|
|
1318
|
+
}
|
|
1319
|
+
</nav>
|
|
1320
|
+
|
|
1321
|
+
<main class="pptx-ng-main">
|
|
1322
|
+
<pptx-slide-canvas
|
|
1323
|
+
[slide]="activeSlide()"
|
|
1324
|
+
[canvasSize]="loader.canvasSize()"
|
|
1325
|
+
[mediaDataUrls]="loader.mediaDataUrls()"
|
|
1326
|
+
[zoom]="zoom()"
|
|
1327
|
+
/>
|
|
1328
|
+
</main>
|
|
1329
|
+
</div>
|
|
1330
|
+
}
|
|
1331
|
+
</div>
|
|
1332
|
+
`,
|
|
1333
|
+
}]
|
|
1334
|
+
}], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], canEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "canEdit", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], collaboration: [{ type: i0.Input, args: [{ isSignal: true, alias: "collaboration", required: false }] }], activeSlideChange: [{ type: i0.Output, args: ["activeSlideChange"] }], dirtyChange: [{ type: i0.Output, args: ["dirtyChange"] }], contentChange: [{ type: i0.Output, args: ["contentChange"] }] } });
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Scalar viewer defaults — shared across the React, Vue, and Angular bindings
|
|
1338
|
+
* via `pptx-viewer-shared`. Re-exported here for ergonomic local imports.
|
|
1339
|
+
*/
|
|
1340
|
+
|
|
1341
|
+
function cn(...values) {
|
|
1342
|
+
return values.filter((v) => Boolean(v)).join(' ');
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Public API surface for `pptx-angular-viewer`.
|
|
1347
|
+
*
|
|
1348
|
+
* Angular counterpart of the React `pptx-viewer` and Vue `pptx-vue-viewer`
|
|
1349
|
+
* packages. Wraps the framework-agnostic `pptx-viewer-core` engine and shares
|
|
1350
|
+
* cross-framework logic via `pptx-viewer-shared`.
|
|
1351
|
+
*/
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Generated bundle index. Do not edit.
|
|
1355
|
+
*/
|
|
1356
|
+
|
|
1357
|
+
export { DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR, ElementRendererComponent, LoadContentService, PowerPointViewerComponent, SlideCanvasComponent, VIEWER_THEME, cn, defaultCssVars, defaultRadius, defaultThemeColors, getContainerStyle, getImageSrc, getShapeFillStrokeStyle, getTextBlockStyle, provideViewerTheme, themeStyle, themeToCssVars };
|
|
1358
|
+
//# sourceMappingURL=pptx-angular-viewer.mjs.map
|