@file-viewer/pptx 2.2.8 → 2.3.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/dist/viewer.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { PptxPresentationState } from './presentation';
1
2
  import type { PptxViewerOptions } from './types';
2
3
  export declare class PptxViewer {
3
4
  static open(buffer: ArrayBuffer, target: HTMLElement, options?: PptxViewerOptions): Promise<PptxViewer>;
@@ -16,6 +17,9 @@ export declare class PptxViewer {
16
17
  private previewThumbnailDataUrl;
17
18
  private slideSize;
18
19
  private slideRecords;
20
+ private totalSlides;
21
+ private savedScroll;
22
+ private pendingScrollRestore;
19
23
  private slideWindowTarget;
20
24
  private slideWindowListeners;
21
25
  private slideWindowFrame;
@@ -24,10 +28,38 @@ export declare class PptxViewer {
24
28
  private readonly mediaRecords;
25
29
  private disposed;
26
30
  private completed;
31
+ private presentation;
27
32
  private readonly handleSlideWindowChange;
28
33
  private constructor();
29
34
  get zoomPercent(): number;
30
35
  get thumbnailDataUrl(): string | null;
36
+ get slideCount(): number;
37
+ get slideDimensions(): {
38
+ width: number;
39
+ height: number;
40
+ } | null;
41
+ get presenting(): boolean;
42
+ get presentationSlideNumber(): number;
43
+ private get styleRoot();
44
+ /**
45
+ * Where the slideshow overlay is mounted. It has to share a root with the injected slide styles,
46
+ * or the engine's scoped CSS stops applying once the slides move into the overlay.
47
+ */
48
+ get presentationRoot(): ShadowRoot | HTMLElement;
49
+ /** Force a slide out of the virtualized window so the slideshow can show it immediately. */
50
+ ensureSlideRendered(slideNumber: number): Element | null;
51
+ refreshLayout(): void;
52
+ /**
53
+ * Remember where the deck was scrolled before the slideshow moves the scale
54
+ * box into the overlay. Moving it collapses the scroller and clamps scrollTop
55
+ * to zero, so the position has to be captured up front and restored on exit.
56
+ */
57
+ saveScrollPosition(): void;
58
+ restoreScrollPosition(): void;
59
+ emitPresentationChange(state: PptxPresentationState): void;
60
+ enterPresentation(slideNumber?: number): Promise<void>;
61
+ exitPresentation(): void;
62
+ togglePresentation(slideNumber?: number): Promise<void>;
31
63
  open(): Promise<void>;
32
64
  setZoom(percent: number): Promise<void>;
33
65
  destroy(): void;
package/dist/viewer.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { renderPptxPostProcessing } from './chart.js';
2
2
  import { resolvePptxEngineOptions, RECOMMENDED_ZIP_LIMITS } from './options.js';
3
- import { ensurePptxViewerStyles, scopePptxContentStyleText } from './styles.js';
3
+ import { PptxPresentation } from './presentation.js';
4
+ import { sanitizePptxCss, sanitizePptxMarkup } from './sanitize.js';
5
+ import { ensurePptxViewerStyles } from './styles.js';
4
6
  import { createPptxWorker } from './worker.js';
5
7
  const clamp = (value, min, max) => {
6
8
  return Math.min(max, Math.max(min, value));
@@ -45,10 +47,9 @@ const ensureZipWithinLimits = (buffer, options) => {
45
47
  }
46
48
  };
47
49
  const appendHtml = (container, html) => {
48
- const template = container.ownerDocument.createElement('template');
49
- template.innerHTML = html;
50
- const nodes = Array.from(template.content.children);
51
- container.append(template.content);
50
+ const fragment = sanitizePptxMarkup(container.ownerDocument, html);
51
+ const nodes = Array.from(fragment.children);
52
+ container.append(fragment);
52
53
  return nodes[0] || null;
53
54
  };
54
55
  const escapeHtml = (value) => String(value !== null && value !== void 0 ? value : '')
@@ -96,6 +97,13 @@ export class PptxViewer {
96
97
  this.previewThumbnailDataUrl = null;
97
98
  this.slideSize = null;
98
99
  this.slideRecords = [];
100
+ // Total slides received from the worker. The windowed path mirrors this in
101
+ // slideRecords, but the default (non-windowed) path appends slides straight
102
+ // into the content node without creating records, so the deck size has to be
103
+ // counted independently of the virtualization bookkeeping.
104
+ this.totalSlides = 0;
105
+ this.savedScroll = null;
106
+ this.pendingScrollRestore = null;
99
107
  this.slideWindowTarget = null;
100
108
  this.slideWindowListeners = [];
101
109
  this.slideWindowFrame = 0;
@@ -104,6 +112,7 @@ export class PptxViewer {
104
112
  this.mediaRecords = new Map();
105
113
  this.disposed = false;
106
114
  this.completed = false;
115
+ this.presentation = null;
107
116
  this.handleSlideWindowChange = () => this.scheduleSlideWindowUpdate();
108
117
  this.buffer = buffer;
109
118
  this.target = target;
@@ -124,9 +133,111 @@ export class PptxViewer {
124
133
  get thumbnailDataUrl() {
125
134
  return this.previewThumbnailDataUrl;
126
135
  }
136
+ get slideCount() {
137
+ return this.totalSlides;
138
+ }
139
+ get slideDimensions() {
140
+ var _a, _b;
141
+ const width = Number((_a = this.slideSize) === null || _a === void 0 ? void 0 : _a.width);
142
+ const height = Number((_b = this.slideSize) === null || _b === void 0 ? void 0 : _b.height);
143
+ return Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0
144
+ ? { width, height }
145
+ : null;
146
+ }
147
+ get presenting() {
148
+ var _a;
149
+ return Boolean((_a = this.presentation) === null || _a === void 0 ? void 0 : _a.active);
150
+ }
151
+ get presentationSlideNumber() {
152
+ var _a, _b;
153
+ return (_b = (_a = this.presentation) === null || _a === void 0 ? void 0 : _a.slideNumber) !== null && _b !== void 0 ? _b : 1;
154
+ }
155
+ get styleRoot() {
156
+ var _a;
157
+ if (this.options.styleRoot) {
158
+ return this.options.styleRoot;
159
+ }
160
+ const documentRef = this.target.ownerDocument || document;
161
+ const root = this.target.getRootNode();
162
+ const ShadowRootCtor = (_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.ShadowRoot;
163
+ return ShadowRootCtor && root instanceof ShadowRootCtor
164
+ ? root
165
+ : documentRef;
166
+ }
167
+ /**
168
+ * Where the slideshow overlay is mounted. It has to share a root with the injected slide styles,
169
+ * or the engine's scoped CSS stops applying once the slides move into the overlay.
170
+ */
171
+ get presentationRoot() {
172
+ const documentRef = this.target.ownerDocument || document;
173
+ const styleRoot = this.styleRoot;
174
+ if (styleRoot !== documentRef) {
175
+ return styleRoot;
176
+ }
177
+ return documentRef.body || documentRef.documentElement;
178
+ }
179
+ /** Force a slide out of the virtualized window so the slideshow can show it immediately. */
180
+ ensureSlideRendered(slideNumber) {
181
+ const record = this.slideRecords.find(item => item.slideNumber === slideNumber);
182
+ if (!record) {
183
+ return null;
184
+ }
185
+ if (!record.rendered) {
186
+ this.renderSlideRecord(record);
187
+ }
188
+ return record.element;
189
+ }
190
+ refreshLayout() {
191
+ this.scheduleResize();
192
+ }
193
+ /**
194
+ * Remember where the deck was scrolled before the slideshow moves the scale
195
+ * box into the overlay. Moving it collapses the scroller and clamps scrollTop
196
+ * to zero, so the position has to be captured up front and restored on exit.
197
+ */
198
+ saveScrollPosition() {
199
+ const view = this.target.ownerDocument.defaultView || window;
200
+ const HTMLElementCtor = view.HTMLElement;
201
+ const scroller = this.slideWindowTarget || this.findSlideWindowTarget();
202
+ if (scroller instanceof HTMLElementCtor) {
203
+ this.savedScroll = { top: scroller.scrollTop, left: scroller.scrollLeft };
204
+ }
205
+ else {
206
+ this.savedScroll = { top: view.scrollY, left: view.scrollX };
207
+ }
208
+ }
209
+ restoreScrollPosition() {
210
+ if (this.savedScroll) {
211
+ this.pendingScrollRestore = this.savedScroll;
212
+ this.savedScroll = null;
213
+ }
214
+ this.scheduleResize();
215
+ }
216
+ emitPresentationChange(state) {
217
+ var _a, _b;
218
+ (_b = (_a = this.options).onPresentationChange) === null || _b === void 0 ? void 0 : _b.call(_a, state);
219
+ }
220
+ async enterPresentation(slideNumber) {
221
+ if (this.disposed || this.totalSlides === 0) {
222
+ return;
223
+ }
224
+ this.presentation || (this.presentation = new PptxPresentation(this, this.options.presentationLabels, this.options.presentationFullscreen));
225
+ await this.presentation.enter(slideNumber);
226
+ }
227
+ exitPresentation() {
228
+ var _a;
229
+ (_a = this.presentation) === null || _a === void 0 ? void 0 : _a.exit();
230
+ }
231
+ async togglePresentation(slideNumber) {
232
+ if (this.presenting) {
233
+ this.exitPresentation();
234
+ return;
235
+ }
236
+ await this.enterPresentation(slideNumber);
237
+ }
127
238
  async open() {
128
239
  ensureZipWithinLimits(this.buffer, this.options);
129
- ensurePptxViewerStyles(this.target.ownerDocument || document, this.options.styleRoot);
240
+ ensurePptxViewerStyles(this.target.ownerDocument || document, this.styleRoot);
130
241
  this.target.replaceChildren(this.scaleBox);
131
242
  this.attachResizeObserver();
132
243
  this.attachSlideWindowListeners();
@@ -138,20 +249,22 @@ export class PptxViewer {
138
249
  this.scheduleResize();
139
250
  }
140
251
  destroy() {
141
- var _a, _b, _c, _d;
252
+ var _a, _b, _c, _d, _e;
142
253
  this.disposed = true;
254
+ (_a = this.presentation) === null || _a === void 0 ? void 0 : _a.destroy();
255
+ this.presentation = null;
143
256
  this.previewThumbnailDataUrl = null;
144
257
  this.releaseCharts();
145
258
  this.releaseMedia();
146
- (_a = this.worker) === null || _a === void 0 ? void 0 : _a.terminate();
259
+ (_b = this.worker) === null || _b === void 0 ? void 0 : _b.terminate();
147
260
  this.worker = null;
148
- (_b = this.resizeObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
261
+ (_c = this.resizeObserver) === null || _c === void 0 ? void 0 : _c.disconnect();
149
262
  this.resizeObserver = null;
150
263
  if (this.resizeFrame) {
151
- (_c = this.target.ownerDocument.defaultView) === null || _c === void 0 ? void 0 : _c.cancelAnimationFrame(this.resizeFrame);
264
+ (_d = this.target.ownerDocument.defaultView) === null || _d === void 0 ? void 0 : _d.cancelAnimationFrame(this.resizeFrame);
152
265
  }
153
266
  if (this.slideWindowFrame) {
154
- (_d = this.target.ownerDocument.defaultView) === null || _d === void 0 ? void 0 : _d.cancelAnimationFrame(this.slideWindowFrame);
267
+ (_e = this.target.ownerDocument.defaultView) === null || _e === void 0 ? void 0 : _e.cancelAnimationFrame(this.slideWindowFrame);
155
268
  }
156
269
  this.detachSlideWindowListeners();
157
270
  this.target.replaceChildren();
@@ -166,6 +279,7 @@ export class PptxViewer {
166
279
  this.previewThumbnailDataUrl = null;
167
280
  this.slideSize = null;
168
281
  this.slideRecords = [];
282
+ this.totalSlides = 0;
169
283
  this.content.replaceChildren();
170
284
  this.content.dataset.renderState = 'loading';
171
285
  try {
@@ -196,6 +310,7 @@ export class PptxViewer {
196
310
  switch (message.type) {
197
311
  case 'slide': {
198
312
  this.clearThumbnail();
313
+ this.totalSlides += 1;
199
314
  if (this.shouldWindowSlides()) {
200
315
  this.appendWindowedSlide(String(message.data || ''), Number(message.slide_num || 0));
201
316
  }
@@ -215,6 +330,7 @@ export class PptxViewer {
215
330
  const error = message.data;
216
331
  const html = buildSlideErrorHtml(slideNumber, error);
217
332
  this.clearThumbnail();
333
+ this.totalSlides += 1;
218
334
  if (this.shouldWindowSlides()) {
219
335
  this.appendWindowedSlide(html, slideNumber);
220
336
  }
@@ -257,11 +373,12 @@ export class PptxViewer {
257
373
  }
258
374
  }
259
375
  appendGlobalCss(css) {
260
- if (!css) {
376
+ const sanitized = sanitizePptxCss(this.target.ownerDocument, css);
377
+ if (!sanitized) {
261
378
  return;
262
379
  }
263
380
  const style = this.target.ownerDocument.createElement('style');
264
- style.textContent = scopePptxContentStyleText(css);
381
+ style.textContent = sanitized;
265
382
  this.content.append(style);
266
383
  }
267
384
  showThumbnail(base64Jpeg) {
@@ -419,6 +536,19 @@ export class PptxViewer {
419
536
  }
420
537
  const windowOptions = this.getSlideWindowOptions();
421
538
  const indexesToRender = this.getWindowedSlideIndexes(windowOptions);
539
+ // While presenting, the active slide and its neighbour live inside the fixed
540
+ // overlay, outside the scroll-view geometry the windowing code measures. Keep
541
+ // them mounted so a resize cannot unmount the slide that is on screen.
542
+ const presentation = this.presentation;
543
+ if (presentation === null || presentation === void 0 ? void 0 : presentation.active) {
544
+ const activeIndex = this.slideRecords.findIndex(record => record.slideNumber === presentation.slideNumber);
545
+ if (activeIndex >= 0) {
546
+ indexesToRender.add(activeIndex);
547
+ if (activeIndex + 1 < this.slideRecords.length) {
548
+ indexesToRender.add(activeIndex + 1);
549
+ }
550
+ }
551
+ }
422
552
  let changed = false;
423
553
  this.slideRecords.forEach((record, index) => {
424
554
  if (indexesToRender.has(index)) {
@@ -747,15 +877,20 @@ export class PptxViewer {
747
877
  this.resizeFrame = view.requestAnimationFrame(() => this.resize());
748
878
  }
749
879
  resize() {
750
- var _a, _b;
880
+ var _a, _b, _c;
881
+ // While presenting, the slideshow owns the transform of the same nodes.
882
+ if ((_a = this.presentation) === null || _a === void 0 ? void 0 : _a.active) {
883
+ this.presentation.layout();
884
+ return;
885
+ }
751
886
  const slides = this.getMountedSlideElements();
752
- const sizeWidth = Number((_a = this.slideSize) === null || _a === void 0 ? void 0 : _a.width);
887
+ const sizeWidth = Number((_b = this.slideSize) === null || _b === void 0 ? void 0 : _b.width);
753
888
  const fallbackWidth = Number.isFinite(sizeWidth) && sizeWidth > 0 ? sizeWidth : 0;
754
889
  const slideWidth = Math.max(...slides.map(slide => slide.offsetWidth), fallbackWidth);
755
890
  if (!slideWidth) {
756
891
  return;
757
892
  }
758
- const viewportWidth = this.target.clientWidth || ((_b = this.target.parentElement) === null || _b === void 0 ? void 0 : _b.clientWidth) || slideWidth;
893
+ const viewportWidth = this.target.clientWidth || ((_c = this.target.parentElement) === null || _c === void 0 ? void 0 : _c.clientWidth) || slideWidth;
759
894
  this.fitScale = this.options.fitMode === 'none'
760
895
  ? 1
761
896
  : Math.min(1, viewportWidth / slideWidth);
@@ -766,6 +901,22 @@ export class PptxViewer {
766
901
  this.scaleBox.style.width = `${Math.ceil(slideWidth * effectiveScale)}px`;
767
902
  this.scaleBox.style.height = `${Math.ceil(this.content.scrollHeight * effectiveScale)}px`;
768
903
  this.scaleBox.style.minHeight = '';
904
+ // The scale box height is recomputed above; only now can the saved scroll
905
+ // position be applied without the scroller clamping it back to zero.
906
+ if (this.pendingScrollRestore) {
907
+ const { top, left } = this.pendingScrollRestore;
908
+ this.pendingScrollRestore = null;
909
+ const view = this.target.ownerDocument.defaultView || window;
910
+ const HTMLElementCtor = view.HTMLElement;
911
+ const scroller = this.slideWindowTarget || this.findSlideWindowTarget();
912
+ if (scroller instanceof HTMLElementCtor) {
913
+ scroller.scrollTop = top;
914
+ scroller.scrollLeft = left;
915
+ }
916
+ else {
917
+ view.scrollTo(left, top);
918
+ }
919
+ }
769
920
  }
770
921
  getMountedSlideElements() {
771
922
  var _a;