@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/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/presentation.d.ts +66 -0
- package/dist/presentation.js +360 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +167 -0
- package/dist/styles.js +18 -0
- package/dist/types.d.ts +4 -0
- package/dist/viewer.d.ts +32 -0
- package/dist/viewer.js +167 -16
- package/dist/worker/pptx.worker.js +6 -6
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { PptxViewer } from './viewer';
|
|
2
|
+
export { PptxPresentation } from './presentation';
|
|
3
|
+
export type { PptxPresentationLabels, PptxPresentationState } from './presentation';
|
|
2
4
|
export { createPptxWorker } from './worker';
|
|
3
5
|
export { RECOMMENDED_ZIP_LIMITS, createDefaultPptxOptions, resolvePptxEngineOptions, } from './options';
|
|
4
6
|
export type { NativePptxEngineOptions, PptxDiagnosticError, PptxDiagnosticErrorCode, PptxFitMode, PptxListOptions, PptxSlideSize, PptxViewerOptions, PptxWorkerFactoryOptions, PptxWorkerMessage, PptxZipLimits, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { PptxViewer } from './viewer';
|
|
2
|
+
export interface PptxPresentationState {
|
|
3
|
+
active: boolean;
|
|
4
|
+
slideNumber: number;
|
|
5
|
+
total: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PptxPresentationLabels {
|
|
8
|
+
exit?: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
next?: string;
|
|
11
|
+
previous?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Full-screen slideshow for a rendered PPTX deck.
|
|
15
|
+
*
|
|
16
|
+
* The slides stay where the viewer put them: the whole scale box is moved into an overlay and the
|
|
17
|
+
* inactive slots are hidden with CSS, so the engine's scoped `.flyfish-pptx-content .slide` rules
|
|
18
|
+
* keep applying and no node is cloned. A placeholder marks the original position so exiting puts
|
|
19
|
+
* everything back exactly where it was.
|
|
20
|
+
*/
|
|
21
|
+
export declare class PptxPresentation {
|
|
22
|
+
private readonly viewer;
|
|
23
|
+
private readonly labels;
|
|
24
|
+
private readonly fullscreen;
|
|
25
|
+
private overlay;
|
|
26
|
+
private stage;
|
|
27
|
+
private counter;
|
|
28
|
+
private placeholder;
|
|
29
|
+
private listeners;
|
|
30
|
+
private layoutFrame;
|
|
31
|
+
private ownsFullscreen;
|
|
32
|
+
private current;
|
|
33
|
+
constructor(viewer: PptxViewer, labels?: PptxPresentationLabels, fullscreen?: boolean);
|
|
34
|
+
get active(): boolean;
|
|
35
|
+
get slideNumber(): number;
|
|
36
|
+
get state(): PptxPresentationState;
|
|
37
|
+
enter(slideNumber?: number): Promise<void>;
|
|
38
|
+
exit(): void;
|
|
39
|
+
toggle(slideNumber?: number): Promise<void>;
|
|
40
|
+
goTo(slideNumber: number): void;
|
|
41
|
+
next(): void;
|
|
42
|
+
previous(): void;
|
|
43
|
+
/** Scale the active slide to fit the overlay, letterboxing whichever axis has slack. */
|
|
44
|
+
layout(): void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
private slots;
|
|
47
|
+
/**
|
|
48
|
+
* The nodes the slideshow toggles between. Windowed decks wrap each slide in a
|
|
49
|
+
* slot; the default (non-windowed) deck appends slides directly to the content
|
|
50
|
+
* node, so those are the containers there.
|
|
51
|
+
*/
|
|
52
|
+
private slideContainers;
|
|
53
|
+
/**
|
|
54
|
+
* Chromium reports the fullscreen element of a shadow root on the shadow root
|
|
55
|
+
* itself while document.fullscreenElement stays on the host, so both have to
|
|
56
|
+
* be checked before deciding who owns fullscreen.
|
|
57
|
+
*/
|
|
58
|
+
private isFullscreenElement;
|
|
59
|
+
private scheduleLayout;
|
|
60
|
+
private notify;
|
|
61
|
+
private on;
|
|
62
|
+
private attach;
|
|
63
|
+
private detach;
|
|
64
|
+
private handleClick;
|
|
65
|
+
private handleKey;
|
|
66
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
2
|
+
/**
|
|
3
|
+
* Full-screen slideshow for a rendered PPTX deck.
|
|
4
|
+
*
|
|
5
|
+
* The slides stay where the viewer put them: the whole scale box is moved into an overlay and the
|
|
6
|
+
* inactive slots are hidden with CSS, so the engine's scoped `.flyfish-pptx-content .slide` rules
|
|
7
|
+
* keep applying and no node is cloned. A placeholder marks the original position so exiting puts
|
|
8
|
+
* everything back exactly where it was.
|
|
9
|
+
*/
|
|
10
|
+
export class PptxPresentation {
|
|
11
|
+
constructor(viewer, labels = {}, fullscreen = true) {
|
|
12
|
+
this.overlay = null;
|
|
13
|
+
this.stage = null;
|
|
14
|
+
this.counter = null;
|
|
15
|
+
this.placeholder = null;
|
|
16
|
+
this.listeners = [];
|
|
17
|
+
this.layoutFrame = 0;
|
|
18
|
+
this.ownsFullscreen = false;
|
|
19
|
+
this.current = 1;
|
|
20
|
+
this.viewer = viewer;
|
|
21
|
+
this.labels = labels;
|
|
22
|
+
this.fullscreen = fullscreen;
|
|
23
|
+
}
|
|
24
|
+
get active() {
|
|
25
|
+
return Boolean(this.overlay);
|
|
26
|
+
}
|
|
27
|
+
get slideNumber() {
|
|
28
|
+
return this.current;
|
|
29
|
+
}
|
|
30
|
+
get state() {
|
|
31
|
+
return { active: this.active, slideNumber: this.current, total: this.viewer.slideCount };
|
|
32
|
+
}
|
|
33
|
+
async enter(slideNumber = this.current) {
|
|
34
|
+
var _a, _b;
|
|
35
|
+
if (this.active || this.viewer.slideCount === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const documentRef = this.viewer.target.ownerDocument || document;
|
|
39
|
+
const overlay = documentRef.createElement('div');
|
|
40
|
+
overlay.className = 'flyfish-pptx-presentation';
|
|
41
|
+
overlay.tabIndex = -1;
|
|
42
|
+
overlay.setAttribute('role', 'region');
|
|
43
|
+
overlay.setAttribute('aria-label', this.labels.hint || 'Slideshow');
|
|
44
|
+
const stage = documentRef.createElement('div');
|
|
45
|
+
stage.className = 'flyfish-pptx-presentation-stage';
|
|
46
|
+
const counter = documentRef.createElement('div');
|
|
47
|
+
counter.className = 'flyfish-pptx-presentation-counter';
|
|
48
|
+
const hint = documentRef.createElement('div');
|
|
49
|
+
hint.className = 'flyfish-pptx-presentation-hint';
|
|
50
|
+
hint.textContent = this.labels.hint || '';
|
|
51
|
+
hint.hidden = !this.labels.hint;
|
|
52
|
+
const exit = documentRef.createElement('button');
|
|
53
|
+
exit.type = 'button';
|
|
54
|
+
exit.className = 'flyfish-pptx-presentation-exit';
|
|
55
|
+
exit.textContent = '✕';
|
|
56
|
+
exit.setAttribute('aria-label', this.labels.exit || 'Exit slideshow');
|
|
57
|
+
exit.addEventListener('click', event => {
|
|
58
|
+
event.stopPropagation();
|
|
59
|
+
this.exit();
|
|
60
|
+
});
|
|
61
|
+
overlay.append(stage, counter, hint, exit);
|
|
62
|
+
this.overlay = overlay;
|
|
63
|
+
this.stage = stage;
|
|
64
|
+
this.counter = counter;
|
|
65
|
+
// Keep the overlay inside whichever root holds the slide styles, or the engine's scoped CSS
|
|
66
|
+
// would not reach the slides once they move.
|
|
67
|
+
const styleRoot = this.viewer.presentationRoot;
|
|
68
|
+
styleRoot.appendChild(overlay);
|
|
69
|
+
// Park a comment where the scale box lived so exit() can restore the exact position.
|
|
70
|
+
this.placeholder = documentRef.createComment('flyfish-pptx-presentation');
|
|
71
|
+
this.viewer.saveScrollPosition();
|
|
72
|
+
this.viewer.scaleBox.replaceWith(this.placeholder);
|
|
73
|
+
stage.appendChild(this.viewer.scaleBox);
|
|
74
|
+
this.viewer.content.classList.add('is-presenting');
|
|
75
|
+
this.attach(documentRef, overlay);
|
|
76
|
+
this.goTo(slideNumber);
|
|
77
|
+
if (this.fullscreen) {
|
|
78
|
+
try {
|
|
79
|
+
await ((_a = overlay.requestFullscreen) === null || _a === void 0 ? void 0 : _a.call(overlay));
|
|
80
|
+
this.ownsFullscreen = this.isFullscreenElement(overlay);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fullscreen needs a user gesture and can be blocked by permissions policy. The overlay is
|
|
84
|
+
// position:fixed, so the slideshow still fills the viewport without it.
|
|
85
|
+
this.ownsFullscreen = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// exit() may have run while fullscreen was being requested. Leave the
|
|
89
|
+
// browser out of fullscreen rather than stranded with no overlay.
|
|
90
|
+
if (this.overlay !== overlay) {
|
|
91
|
+
if (this.ownsFullscreen) {
|
|
92
|
+
void ((_b = documentRef.exitFullscreen) === null || _b === void 0 ? void 0 : _b.call(documentRef).catch(() => undefined));
|
|
93
|
+
}
|
|
94
|
+
this.ownsFullscreen = false;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
overlay.focus({ preventScroll: true });
|
|
98
|
+
this.scheduleLayout();
|
|
99
|
+
this.notify();
|
|
100
|
+
}
|
|
101
|
+
exit() {
|
|
102
|
+
var _a, _b;
|
|
103
|
+
const overlay = this.overlay;
|
|
104
|
+
if (!overlay) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const documentRef = this.viewer.target.ownerDocument || document;
|
|
108
|
+
// Capture before the overlay leaves the DOM: a detached node's getRootNode()
|
|
109
|
+
// is the document, so the fullscreen check would fail after removal.
|
|
110
|
+
const shouldExitFullscreen = this.ownsFullscreen && this.isFullscreenElement(overlay);
|
|
111
|
+
this.detach();
|
|
112
|
+
if (this.layoutFrame) {
|
|
113
|
+
(documentRef.defaultView || window).cancelAnimationFrame(this.layoutFrame);
|
|
114
|
+
this.layoutFrame = 0;
|
|
115
|
+
}
|
|
116
|
+
this.viewer.content.classList.remove('is-presenting');
|
|
117
|
+
for (const container of this.slideContainers()) {
|
|
118
|
+
container.classList.remove('is-active-slide');
|
|
119
|
+
}
|
|
120
|
+
if ((_a = this.placeholder) === null || _a === void 0 ? void 0 : _a.parentNode) {
|
|
121
|
+
this.placeholder.replaceWith(this.viewer.scaleBox);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.viewer.target.appendChild(this.viewer.scaleBox);
|
|
125
|
+
}
|
|
126
|
+
this.placeholder = null;
|
|
127
|
+
overlay.remove();
|
|
128
|
+
this.overlay = null;
|
|
129
|
+
this.stage = null;
|
|
130
|
+
this.counter = null;
|
|
131
|
+
if (shouldExitFullscreen) {
|
|
132
|
+
void ((_b = documentRef.exitFullscreen) === null || _b === void 0 ? void 0 : _b.call(documentRef).catch(() => undefined));
|
|
133
|
+
}
|
|
134
|
+
this.ownsFullscreen = false;
|
|
135
|
+
this.viewer.restoreScrollPosition();
|
|
136
|
+
this.notify();
|
|
137
|
+
}
|
|
138
|
+
async toggle(slideNumber) {
|
|
139
|
+
if (this.active) {
|
|
140
|
+
this.exit();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await this.enter(slideNumber);
|
|
144
|
+
}
|
|
145
|
+
goTo(slideNumber) {
|
|
146
|
+
const total = this.viewer.slideCount;
|
|
147
|
+
if (total === 0) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.current = clamp(Math.round(slideNumber) || 1, 1, total);
|
|
151
|
+
this.viewer.ensureSlideRendered(this.current);
|
|
152
|
+
// Rendering the neighbour keeps the next step instant without defeating virtualization.
|
|
153
|
+
this.viewer.ensureSlideRendered(this.current + 1);
|
|
154
|
+
const containers = this.slideContainers();
|
|
155
|
+
// Windowed decks tag each slot with its slide number; non-windowed decks
|
|
156
|
+
// append slides in order, so the active one is found by index.
|
|
157
|
+
const byNumber = containers.some(container => Boolean(container.dataset.slideNumber));
|
|
158
|
+
containers.forEach((container, index) => {
|
|
159
|
+
const isActive = byNumber
|
|
160
|
+
? Number(container.dataset.slideNumber) === this.current
|
|
161
|
+
: index === this.current - 1;
|
|
162
|
+
container.classList.toggle('is-active-slide', isActive);
|
|
163
|
+
});
|
|
164
|
+
if (this.counter) {
|
|
165
|
+
this.counter.textContent = `${this.current} / ${total}`;
|
|
166
|
+
}
|
|
167
|
+
if (this.active) {
|
|
168
|
+
this.scheduleLayout();
|
|
169
|
+
this.notify();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
next() {
|
|
173
|
+
if (this.current < this.viewer.slideCount) {
|
|
174
|
+
this.goTo(this.current + 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
previous() {
|
|
178
|
+
if (this.current > 1) {
|
|
179
|
+
this.goTo(this.current - 1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Scale the active slide to fit the overlay, letterboxing whichever axis has slack. */
|
|
183
|
+
layout() {
|
|
184
|
+
const overlay = this.overlay;
|
|
185
|
+
if (!overlay) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const container = this.slideContainers().find((item, index) => {
|
|
189
|
+
if (item.dataset.slideNumber) {
|
|
190
|
+
return Number(item.dataset.slideNumber) === this.current;
|
|
191
|
+
}
|
|
192
|
+
return index === this.current - 1;
|
|
193
|
+
});
|
|
194
|
+
// Windowed decks wrap the slide in a slot; non-windowed decks are the slide.
|
|
195
|
+
const slide = container && container.classList.contains('flyfish-pptx-slide-slot')
|
|
196
|
+
? container.firstElementChild
|
|
197
|
+
: container;
|
|
198
|
+
const size = this.viewer.slideDimensions;
|
|
199
|
+
const slideWidth = (slide === null || slide === void 0 ? void 0 : slide.offsetWidth) || (size === null || size === void 0 ? void 0 : size.width) || 0;
|
|
200
|
+
const slideHeight = (slide === null || slide === void 0 ? void 0 : slide.offsetHeight) || (size === null || size === void 0 ? void 0 : size.height) || 0;
|
|
201
|
+
if (!slideWidth || !slideHeight) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const viewWidth = overlay.clientWidth;
|
|
205
|
+
const viewHeight = overlay.clientHeight;
|
|
206
|
+
const scale = Math.min(viewWidth / slideWidth, viewHeight / slideHeight);
|
|
207
|
+
const offsetX = Math.max(0, (viewWidth - slideWidth * scale) / 2);
|
|
208
|
+
const offsetY = Math.max(0, (viewHeight - slideHeight * scale) / 2);
|
|
209
|
+
const content = this.viewer.content;
|
|
210
|
+
content.style.width = `${slideWidth}px`;
|
|
211
|
+
content.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
|
212
|
+
const scaleBox = this.viewer.scaleBox;
|
|
213
|
+
scaleBox.style.width = '100%';
|
|
214
|
+
scaleBox.style.height = '100%';
|
|
215
|
+
scaleBox.style.minHeight = '';
|
|
216
|
+
}
|
|
217
|
+
destroy() {
|
|
218
|
+
this.exit();
|
|
219
|
+
}
|
|
220
|
+
slots() {
|
|
221
|
+
return Array.from(this.viewer.content.querySelectorAll(':scope > .flyfish-pptx-slide-slot'));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The nodes the slideshow toggles between. Windowed decks wrap each slide in a
|
|
225
|
+
* slot; the default (non-windowed) deck appends slides directly to the content
|
|
226
|
+
* node, so those are the containers there.
|
|
227
|
+
*/
|
|
228
|
+
slideContainers() {
|
|
229
|
+
const slots = this.slots();
|
|
230
|
+
if (slots.length > 0) {
|
|
231
|
+
return slots;
|
|
232
|
+
}
|
|
233
|
+
return Array.from(this.viewer.content.querySelectorAll(':scope > .slide'));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Chromium reports the fullscreen element of a shadow root on the shadow root
|
|
237
|
+
* itself while document.fullscreenElement stays on the host, so both have to
|
|
238
|
+
* be checked before deciding who owns fullscreen.
|
|
239
|
+
*/
|
|
240
|
+
isFullscreenElement(overlay) {
|
|
241
|
+
const documentRef = this.viewer.target.ownerDocument || document;
|
|
242
|
+
if (documentRef.fullscreenElement === overlay) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
const root = overlay.getRootNode();
|
|
246
|
+
if (root !== documentRef && 'fullscreenElement' in root) {
|
|
247
|
+
return root.fullscreenElement === overlay;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
scheduleLayout() {
|
|
252
|
+
const view = this.viewer.target.ownerDocument.defaultView || window;
|
|
253
|
+
if (this.layoutFrame) {
|
|
254
|
+
view.cancelAnimationFrame(this.layoutFrame);
|
|
255
|
+
}
|
|
256
|
+
this.layoutFrame = view.requestAnimationFrame(() => {
|
|
257
|
+
this.layoutFrame = 0;
|
|
258
|
+
this.layout();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
notify() {
|
|
262
|
+
this.viewer.emitPresentationChange(this.state);
|
|
263
|
+
}
|
|
264
|
+
on(target, type, listener) {
|
|
265
|
+
target.addEventListener(type, listener);
|
|
266
|
+
this.listeners.push({ target, type, listener });
|
|
267
|
+
}
|
|
268
|
+
attach(documentRef, overlay) {
|
|
269
|
+
const view = documentRef.defaultView || window;
|
|
270
|
+
this.on(documentRef, 'keydown', (event) => this.handleKey(event));
|
|
271
|
+
this.on(overlay, 'click', (event) => this.handleClick(event));
|
|
272
|
+
this.on(overlay, 'contextmenu', (event) => {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
this.previous();
|
|
275
|
+
});
|
|
276
|
+
this.on(view, 'resize', () => this.scheduleLayout());
|
|
277
|
+
this.on(documentRef, 'fullscreenchange', () => {
|
|
278
|
+
// Leaving fullscreen with the browser's own Esc must close the slideshow too, otherwise the
|
|
279
|
+
// overlay would stay up as a plain fixed layer.
|
|
280
|
+
if (this.ownsFullscreen && !this.isFullscreenElement(overlay)) {
|
|
281
|
+
this.ownsFullscreen = false;
|
|
282
|
+
this.exit();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
detach() {
|
|
287
|
+
for (const { target, type, listener } of this.listeners) {
|
|
288
|
+
target.removeEventListener(type, listener);
|
|
289
|
+
}
|
|
290
|
+
this.listeners = [];
|
|
291
|
+
}
|
|
292
|
+
handleClick(event) {
|
|
293
|
+
var _a;
|
|
294
|
+
const overlay = this.overlay;
|
|
295
|
+
if (!overlay || ((_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('.flyfish-pptx-presentation-exit'))) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// PowerPoint advances on click; the left edge is the only place that goes back.
|
|
299
|
+
const bounds = overlay.getBoundingClientRect();
|
|
300
|
+
const ratio = (event.clientX - bounds.left) / (bounds.width || 1);
|
|
301
|
+
if (ratio < 0.2) {
|
|
302
|
+
this.previous();
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
this.next();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
handleKey(event) {
|
|
309
|
+
if (!this.active || event.altKey || event.metaKey || event.ctrlKey) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Enter/Space on a focused control (the exit button) must activate that
|
|
313
|
+
// control, not advance the slide. Let the browser's default run instead.
|
|
314
|
+
const target = event.target;
|
|
315
|
+
if (target &&
|
|
316
|
+
typeof target.closest === 'function' &&
|
|
317
|
+
target.closest('button, a, input, textarea, select, [contenteditable="true"], [contenteditable=""]')) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// With two slideshows open, a key pressed inside the other overlay belongs
|
|
321
|
+
// to that overlay; only the focused one advances.
|
|
322
|
+
if (target && typeof target.closest === 'function') {
|
|
323
|
+
const otherOverlay = target.closest('.flyfish-pptx-presentation');
|
|
324
|
+
if (otherOverlay && otherOverlay !== this.overlay) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
switch (event.key) {
|
|
329
|
+
case 'ArrowRight':
|
|
330
|
+
case 'ArrowDown':
|
|
331
|
+
case 'PageDown':
|
|
332
|
+
case ' ':
|
|
333
|
+
case 'Enter':
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
this.next();
|
|
336
|
+
break;
|
|
337
|
+
case 'ArrowLeft':
|
|
338
|
+
case 'ArrowUp':
|
|
339
|
+
case 'PageUp':
|
|
340
|
+
case 'Backspace':
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
this.previous();
|
|
343
|
+
break;
|
|
344
|
+
case 'Home':
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
this.goTo(1);
|
|
347
|
+
break;
|
|
348
|
+
case 'End':
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
this.goTo(this.viewer.slideCount);
|
|
351
|
+
break;
|
|
352
|
+
case 'Escape':
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
this.exit();
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import createDOMPurify from 'dompurify';
|
|
2
|
+
const PPTX_CONTENT_STYLE_SCOPE = '.flyfish-pptx-content';
|
|
3
|
+
const GENERATED_STYLE_RULE = /\s*(\._(?:css|svg_css|tbl_cell_css)_[A-Za-z0-9_-]+)\s*\{([^{}]*)\}/y;
|
|
4
|
+
const purifierByDocument = new WeakMap();
|
|
5
|
+
const createSafeFallbackFragment = (documentRef) => {
|
|
6
|
+
const fragment = documentRef.createDocumentFragment();
|
|
7
|
+
const fallback = documentRef.createElement('section');
|
|
8
|
+
fallback.className = 'slide flyfish-pptx-slide-error';
|
|
9
|
+
fallback.textContent = 'This slide could not be displayed safely.';
|
|
10
|
+
fragment.append(fallback);
|
|
11
|
+
return fragment;
|
|
12
|
+
};
|
|
13
|
+
const getPurifier = (documentRef) => {
|
|
14
|
+
const cached = purifierByDocument.get(documentRef);
|
|
15
|
+
if (cached) {
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
const windowRef = documentRef.defaultView;
|
|
19
|
+
if (!windowRef) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const purifier = createDOMPurify(windowRef);
|
|
23
|
+
if (!purifier.isSupported) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
purifierByDocument.set(documentRef, purifier);
|
|
27
|
+
return purifier;
|
|
28
|
+
};
|
|
29
|
+
const isAllowedCssUrl = (value) => {
|
|
30
|
+
const target = value
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2')
|
|
33
|
+
.trim();
|
|
34
|
+
if (/^#[A-Za-z0-9_.:-]+$/.test(target)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return /^data:image\/[A-Za-z0-9.+-]+;base64,[A-Za-z0-9+/=\s]+$/i.test(target);
|
|
38
|
+
};
|
|
39
|
+
const isSafeEmbeddedResourceUrl = (value, allowFragment) => {
|
|
40
|
+
const normalized = value.trim();
|
|
41
|
+
if (allowFragment && /^#[A-Za-z0-9_.:-]+$/.test(normalized)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (/^blob:/i.test(normalized)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return /^data:image\/[A-Za-z0-9.+-]+;base64,[A-Za-z0-9+/=\s]+$/i.test(normalized);
|
|
48
|
+
};
|
|
49
|
+
const isSafeCssValue = (value) => {
|
|
50
|
+
const normalized = value.replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
51
|
+
if (/(?:expression|image(?:-set)?|paint|var)\s*\(/i.test(normalized) ||
|
|
52
|
+
/(?:javascript|vbscript)\s*:/i.test(normalized) ||
|
|
53
|
+
/(?:^|[^-])behavior\s*:/i.test(normalized) ||
|
|
54
|
+
/-moz-binding/i.test(normalized) ||
|
|
55
|
+
normalized.includes('\\')) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
let unsafeUrl = false;
|
|
59
|
+
const withoutUrls = normalized.replace(/url\(\s*([^)]*?)\s*\)/gi, (_match, target) => {
|
|
60
|
+
if (!isAllowedCssUrl(target)) {
|
|
61
|
+
unsafeUrl = true;
|
|
62
|
+
}
|
|
63
|
+
return '';
|
|
64
|
+
});
|
|
65
|
+
return !unsafeUrl && !/url\s*\(/i.test(withoutUrls);
|
|
66
|
+
};
|
|
67
|
+
const sanitizeStyleDeclaration = (documentRef, cssText) => {
|
|
68
|
+
const probe = documentRef.createElement('span');
|
|
69
|
+
probe.setAttribute('style', cssText);
|
|
70
|
+
const style = probe.style;
|
|
71
|
+
const propertyNames = Array.from({ length: style.length }, (_, index) => style.item(index));
|
|
72
|
+
for (const propertyName of propertyNames) {
|
|
73
|
+
const value = style.getPropertyValue(propertyName);
|
|
74
|
+
if (!propertyName ||
|
|
75
|
+
propertyName.startsWith('--') ||
|
|
76
|
+
propertyName === 'behavior' ||
|
|
77
|
+
propertyName === '-moz-binding' ||
|
|
78
|
+
(propertyName === 'position' && /^(?:fixed|sticky)$/i.test(value.trim())) ||
|
|
79
|
+
!isSafeCssValue(value)) {
|
|
80
|
+
style.removeProperty(propertyName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return style.cssText;
|
|
84
|
+
};
|
|
85
|
+
const sanitizeStyleAttributes = (documentRef, root) => {
|
|
86
|
+
root.querySelectorAll('[style]').forEach((element) => {
|
|
87
|
+
const sanitized = sanitizeStyleDeclaration(documentRef, element.getAttribute('style') || '');
|
|
88
|
+
if (sanitized) {
|
|
89
|
+
element.setAttribute('style', sanitized);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
element.removeAttribute('style');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const sanitizeEmbeddedResourceUrls = (root) => {
|
|
97
|
+
root
|
|
98
|
+
.querySelectorAll('[src],[srcset],[poster],[href],[xlink\\:href]')
|
|
99
|
+
.forEach((element) => {
|
|
100
|
+
for (const attributeName of ['src', 'srcset', 'poster', 'href', 'xlink:href']) {
|
|
101
|
+
if (!element.hasAttribute(attributeName)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const isHtmlAnchor = element.namespaceURI === 'http://www.w3.org/1999/xhtml' && element.localName === 'a';
|
|
105
|
+
if (isHtmlAnchor && attributeName === 'href') {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const allowFragment = element.namespaceURI === 'http://www.w3.org/2000/svg' &&
|
|
109
|
+
(attributeName === 'href' || attributeName === 'xlink:href');
|
|
110
|
+
if (!isSafeEmbeddedResourceUrl(element.getAttribute(attributeName) || '', allowFragment)) {
|
|
111
|
+
element.removeAttribute(attributeName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const sanitizeSvgUrlAttributes = (root) => {
|
|
117
|
+
root.querySelectorAll('svg, svg *').forEach((element) => {
|
|
118
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
119
|
+
const value = attribute.value;
|
|
120
|
+
if (/url\s*\(/i.test(value) && !isSafeCssValue(value)) {
|
|
121
|
+
element.removeAttribute(attribute.name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
export const sanitizePptxCss = (documentRef, cssText) => {
|
|
127
|
+
const rules = [];
|
|
128
|
+
let offset = 0;
|
|
129
|
+
while (offset < cssText.length) {
|
|
130
|
+
if (!cssText.slice(offset).trim()) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
GENERATED_STYLE_RULE.lastIndex = offset;
|
|
134
|
+
const match = GENERATED_STYLE_RULE.exec(cssText);
|
|
135
|
+
if (!match) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
const declarations = sanitizeStyleDeclaration(documentRef, match[2]);
|
|
139
|
+
if (declarations) {
|
|
140
|
+
rules.push(`${PPTX_CONTENT_STYLE_SCOPE} ${match[1]}{${declarations}}`);
|
|
141
|
+
}
|
|
142
|
+
offset = GENERATED_STYLE_RULE.lastIndex;
|
|
143
|
+
}
|
|
144
|
+
return rules.join('\n');
|
|
145
|
+
};
|
|
146
|
+
export const sanitizePptxMarkup = (documentRef, html) => {
|
|
147
|
+
const purifier = getPurifier(documentRef);
|
|
148
|
+
if (!purifier) {
|
|
149
|
+
return createSafeFallbackFragment(documentRef);
|
|
150
|
+
}
|
|
151
|
+
const fragment = purifier.sanitize(html, {
|
|
152
|
+
RETURN_DOM_FRAGMENT: true,
|
|
153
|
+
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
|
154
|
+
ADD_ATTR: ['target'],
|
|
155
|
+
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
|
|
156
|
+
FORBID_ATTR: ['srcdoc']
|
|
157
|
+
});
|
|
158
|
+
sanitizeStyleAttributes(documentRef, fragment);
|
|
159
|
+
sanitizeEmbeddedResourceUrls(fragment);
|
|
160
|
+
sanitizeSvgUrlAttributes(fragment);
|
|
161
|
+
fragment.querySelectorAll('a[target]').forEach((anchor) => {
|
|
162
|
+
if ((anchor.getAttribute('target') || '').trim().toLowerCase() === '_blank') {
|
|
163
|
+
anchor.rel = 'noopener noreferrer';
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return fragment;
|
|
167
|
+
};
|
package/dist/styles.js
CHANGED
|
@@ -57,6 +57,24 @@ export const pptxViewerCss = `
|
|
|
57
57
|
${scopePptxContentStyleText(legacyPptxCss)}
|
|
58
58
|
.flyfish-pptx-content > .slide:last-of-type{margin-bottom:0}
|
|
59
59
|
.flyfish-pptx-slide-slot:last-of-type > .slide{margin-bottom:0}
|
|
60
|
+
.flyfish-pptx-presentation{position:fixed;inset:0;z-index:2147483000;display:block;overflow:hidden;background:#000;cursor:default;user-select:none;-webkit-user-select:none}
|
|
61
|
+
.flyfish-pptx-presentation:focus{outline:none}
|
|
62
|
+
.flyfish-pptx-presentation-stage{position:absolute;inset:0;overflow:hidden}
|
|
63
|
+
.flyfish-pptx-presentation .flyfish-pptx-scale-box{position:absolute;inset:0;width:100%;height:100%;margin:0}
|
|
64
|
+
.flyfish-pptx-presentation .flyfish-pptx-thumbnail{display:none}
|
|
65
|
+
.flyfish-pptx-content.is-presenting > .flyfish-pptx-slide-slot{display:none;min-height:0}
|
|
66
|
+
.flyfish-pptx-content.is-presenting > .flyfish-pptx-slide-slot.is-active-slide{display:flow-root}
|
|
67
|
+
.flyfish-pptx-content.is-presenting > .slide{display:none}
|
|
68
|
+
.flyfish-pptx-content.is-presenting > .slide.is-active-slide{display:block}
|
|
69
|
+
.flyfish-pptx-content.is-presenting .slide{margin:0!important;box-shadow:none}
|
|
70
|
+
.flyfish-pptx-presentation-counter{position:absolute;right:18px;bottom:14px;padding:5px 12px;border-radius:999px;background:rgba(255,255,255,.14);color:#fff;font:13px/1.4 system-ui,sans-serif;letter-spacing:.02em;pointer-events:none}
|
|
71
|
+
.flyfish-pptx-presentation-hint{position:absolute;left:18px;bottom:14px;color:rgba(255,255,255,.55);font:12px/1.4 system-ui,sans-serif;pointer-events:none}
|
|
72
|
+
.flyfish-pptx-presentation-hint[hidden]{display:none}
|
|
73
|
+
.flyfish-pptx-presentation-exit{position:absolute;top:14px;right:14px;width:34px;height:34px;padding:0;border:0;border-radius:999px;background:rgba(255,255,255,.14);color:#fff;font-size:15px;line-height:1;cursor:pointer}
|
|
74
|
+
.flyfish-pptx-presentation-exit:hover{background:rgba(255,255,255,.26)}
|
|
75
|
+
.flyfish-pptx-presentation-exit:focus-visible{outline:2px solid #fff;outline-offset:2px}
|
|
76
|
+
@media (prefers-reduced-motion:no-preference){.flyfish-pptx-content.is-presenting > .flyfish-pptx-slide-slot.is-active-slide,.flyfish-pptx-content.is-presenting > .slide.is-active-slide{animation:flyfish-pptx-slide-in 140ms ease-out}}
|
|
77
|
+
@keyframes flyfish-pptx-slide-in{from{opacity:.35}to{opacity:1}}
|
|
60
78
|
`;
|
|
61
79
|
const PPTX_STYLE_ID = 'flyfish-pptx-native-style';
|
|
62
80
|
export const ensurePptxViewerStyles = (documentRef, root = documentRef) => {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PptxPresentationLabels, PptxPresentationState } from './presentation';
|
|
1
2
|
export type PptxFitMode = 'contain' | 'none';
|
|
2
3
|
export interface PptxZipLimits {
|
|
3
4
|
maxFileBytes?: number;
|
|
@@ -60,6 +61,9 @@ export interface PptxViewerOptions extends PptxWorkerFactoryOptions {
|
|
|
60
61
|
onRenderComplete?: () => void;
|
|
61
62
|
onWarning?: (warning: unknown) => void;
|
|
62
63
|
onError?: (error: unknown) => void;
|
|
64
|
+
presentationLabels?: PptxPresentationLabels;
|
|
65
|
+
presentationFullscreen?: boolean;
|
|
66
|
+
onPresentationChange?: (state: PptxPresentationState) => void;
|
|
63
67
|
}
|
|
64
68
|
export interface PptxWorkerMessage {
|
|
65
69
|
type: string;
|