返回 reveal.js
overlay.js
根目录 / js / controllers / overlay.js
1 /**
2 * Handles the display of reveal.js' overlay elements used
3 * to preview iframes, images & videos.
4 */
5 export default class Overlay {
6
7 constructor( Reveal ) {
8
9 this.Reveal = Reveal;
10
11 this.onSlidesClicked = this.onSlidesClicked.bind( this );
12
13 this.iframeTriggerSelector = null;
14 this.mediaTriggerSelector = '[data-preview-image], [data-preview-video]';
15
16 this.stateProps = ['previewIframe', 'previewImage', 'previewVideo', 'previewFit'];
17 this.state = {};
18
19 }
20
21 update() {
22
23 // Enable link previews globally
24 if( this.Reveal.getConfig().previewLinks ) {
25 this.iframeTriggerSelector = 'a[href]:not([data-preview-link=false]), [data-preview-link]:not(a):not([data-preview-link=false])';
26 }
27 // Enable link previews for individual elements
28 else {
29 this.iframeTriggerSelector = '[data-preview-link]:not([data-preview-link=false])';
30 }
31
32 const hasLinkPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.iframeTriggerSelector ).length > 0;
33 const hasMediaPreviews = this.Reveal.getSlidesElement().querySelectorAll( this.mediaTriggerSelector ).length > 0;
34
35 // Only add the listener when there are previewable elements in the slides
36 if( hasLinkPreviews || hasMediaPreviews ) {
37 this.Reveal.getSlidesElement().addEventListener( 'click', this.onSlidesClicked, false );
38 }
39 else {
40 this.Reveal.getSlidesElement().removeEventListener( 'click', this.onSlidesClicked, false );
41 }
42
43 }
44
45 createOverlay( className ) {
46
47 this.dom = document.createElement( 'div' );
48 this.dom.classList.add( 'r-overlay' );
49 this.dom.classList.add( className );
50
51 this.viewport = document.createElement( 'div' );
52 this.viewport.classList.add( 'r-overlay-viewport' );
53
54 this.dom.appendChild( this.viewport );
55 this.Reveal.getRevealElement().appendChild( this.dom );
56
57 }
58
59 /**
60 * Opens a lightbox that previews the target URL.
61 *
62 * @param {string} url - url for lightbox iframe src
63 */
64 previewIframe( url ) {
65
66 this.close();
67
68 this.state = { previewIframe: url };
69
70 this.createOverlay( 'r-overlay-preview' );
71 this.dom.dataset.state = 'loading';
72
73 this.viewport.innerHTML =
74 `<header class="r-overlay-header">
75 <a class="r-overlay-header-button r-overlay-external" href="${url}" target="_blank"><span class="icon"></span></a>
76 <button class="r-overlay-header-button r-overlay-close"><span class="icon"></span></button>
77 </header>
78 <div class="r-overlay-spinner"></div>
79 <div class="r-overlay-content">
80 <iframe src="${url}"></iframe>
81 <small class="r-overlay-content-inner">
82 <span class="r-overlay-error x-frame-error">Unable to load iframe. This is likely due to the site's policy (x-frame-options).</span>
83 </small>
84 </div>`;
85
86 this.dom.querySelector( 'iframe' ).addEventListener( 'load', event => {
87 this.dom.dataset.state = 'loaded';
88 }, false );
89
90 this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => {
91 this.close();
92 event.preventDefault();
93 }, false );
94
95 this.dom.querySelector( '.r-overlay-external' ).addEventListener( 'click', event => {
96 this.close();
97 }, false );
98
99 this.Reveal.dispatchEvent({ type: 'previewiframe', data: { url } });
100
101 }
102
103 /**
104 * Opens a lightbox window that provides a larger view of the
105 * given image/video.
106 *
107 * @param {string} url - url to the image/video to preview
108 * @param {image|video} mediaType
109 * @param {string} [fitMode] - the fit mode to use for the preview
110 */
111 previewMedia( url, mediaType, fitMode ) {
112
113 if( mediaType !== 'image' && mediaType !== 'video' ) {
114 console.warn( 'Please specify a valid media type to preview (image|video)' );
115 return;
116 }
117
118 this.close();
119
120 fitMode = fitMode || 'scale-down';
121
122 this.createOverlay( 'r-overlay-preview' );
123 this.dom.dataset.state = 'loading';
124 this.dom.dataset.previewFit = fitMode;
125
126 this.viewport.innerHTML =
127 `<header class="r-overlay-header">
128 <button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
129 </header>
130 <div class="r-overlay-spinner"></div>
131 <div class="r-overlay-content"></div>`;
132
133 const contentElement = this.dom.querySelector( '.r-overlay-content' );
134
135 if( mediaType === 'image' ) {
136
137 this.state = { previewImage: url, previewFit: fitMode }
138
139 const img = document.createElement( 'img', {} );
140 img.src = url;
141 contentElement.appendChild( img );
142
143 img.addEventListener( 'load', () => {
144 this.dom.dataset.state = 'loaded';
145 }, false );
146
147 img.addEventListener( 'error', () => {
148 this.dom.dataset.state = 'error';
149 contentElement.innerHTML =
150 `<span class="r-overlay-error">Unable to load image.</span>`
151 }, false );
152
153 // Hide image overlays when clicking outside the overlay
154 this.dom.style.cursor = 'zoom-out';
155 this.dom.addEventListener( 'click', ( event ) => {
156 this.close();
157 }, false );
158
159 this.Reveal.dispatchEvent({ type: 'previewimage', data: { url } });
160
161 }
162 else if( mediaType === 'video' ) {
163
164 this.state = { previewVideo: url, previewFit: fitMode }
165
166 const video = document.createElement( 'video' );
167 video.autoplay = this.dom.dataset.previewAutoplay === 'false' ? false : true;
168 video.controls = this.dom.dataset.previewControls === 'false' ? false : true;
169 video.loop = this.dom.dataset.previewLoop === 'true' ? true : false;
170 video.muted = this.dom.dataset.previewMuted === 'true' ? true : false;
171 video.playsInline = true;
172 video.src = url;
173 contentElement.appendChild( video );
174
175 video.addEventListener( 'loadeddata', () => {
176 this.dom.dataset.state = 'loaded';
177 }, false );
178
179 video.addEventListener( 'error', () => {
180 this.dom.dataset.state = 'error';
181 contentElement.innerHTML =
182 `<span class="r-overlay-error">Unable to load video.</span>`;
183 }, false );
184
185 this.Reveal.dispatchEvent({ type: 'previewvideo', data: { url } });
186
187 }
188 else {
189 throw new Error( 'Please specify a valid media type to preview' );
190 }
191
192 this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', ( event ) => {
193 this.close();
194 event.preventDefault();
195 }, false );
196
197 }
198
199 previewImage( url, fitMode ) {
200
201 this.previewMedia( url, 'image', fitMode );
202
203 }
204
205 previewVideo( url, fitMode ) {
206
207 this.previewMedia( url, 'video', fitMode );
208
209 }
210
211 /**
212 * Open or close help overlay window.
213 *
214 * @param {Boolean} [override] Flag which overrides the
215 * toggle logic and forcibly sets the desired state. True means
216 * help is open, false means it's closed.
217 */
218 toggleHelp( override ) {
219
220 if( typeof override === 'boolean' ) {
221 override ? this.showHelp() : this.close();
222 }
223 else {
224 if( this.dom ) {
225 this.close();
226 }
227 else {
228 this.showHelp();
229 }
230 }
231 }
232
233 /**
234 * Opens an overlay window with help material.
235 */
236 showHelp() {
237
238 if( this.Reveal.getConfig().help ) {
239
240 this.close();
241
242 this.createOverlay( 'r-overlay-help' );
243
244 let html = '<p class="title">Keyboard Shortcuts</p>';
245
246 let shortcuts = this.Reveal.keyboard.getShortcuts(),
247 bindings = this.Reveal.keyboard.getBindings();
248
249 html += '<table><th>KEY</th><th>ACTION</th>';
250 for( let key in shortcuts ) {
251 html += `<tr><td>${key}</td><td>${shortcuts[ key ]}</td></tr>`;
252 }
253
254 // Add custom key bindings that have associated descriptions
255 for( let binding in bindings ) {
256 if( bindings[binding].key && bindings[binding].description ) {
257 html += `<tr><td>${bindings[binding].key}</td><td>${bindings[binding].description}</td></tr>`;
258 }
259 }
260
261 html += '</table>';
262
263 this.viewport.innerHTML = `
264 <header class="r-overlay-header">
265 <button class="r-overlay-header-button r-overlay-close">Esc <span class="icon"></span></button>
266 </header>
267 <div class="r-overlay-content">
268 <div class="r-overlay-help-content">${html}</div>
269 </div>
270 `;
271
272 this.dom.querySelector( '.r-overlay-close' ).addEventListener( 'click', event => {
273 this.close();
274 event.preventDefault();
275 }, false );
276
277 this.Reveal.dispatchEvent({ type: 'showhelp' });
278
279 }
280
281 }
282
283 isOpen() {
284
285 return !!this.dom;
286
287 }
288
289 /**
290 * Closes any currently open overlay.
291 */
292 close() {
293
294 if( this.dom ) {
295 this.dom.remove();
296 this.dom = null;
297
298 this.state = {};
299
300 this.Reveal.dispatchEvent({ type: 'closeoverlay' });
301
302 return true;
303 }
304
305 return false;
306
307 }
308
309 getState() {
310
311 return this.state;
312
313 }
314
315 setState( state ) {
316
317 // Ignore the incoming state if none of the preview related
318 // props have changed
319 if( this.stateProps.every( key => this.state[ key ] === state[ key ] ) ) {
320 return;
321 }
322
323 if( state.previewIframe ) {
324 this.previewIframe( state.previewIframe );
325 }
326 else if( state.previewImage ) {
327 this.previewImage( state.previewImage, state.previewFit );
328 }
329 else if( state.previewVideo ) {
330 this.previewVideo( state.previewVideo, state.previewFit );
331 }
332 else {
333 this.close();
334 }
335
336 }
337
338 onSlidesClicked( event ) {
339
340 const target = event.target;
341
342 const linkTarget = target.closest( this.iframeTriggerSelector );
343 const mediaTarget = target.closest( this.mediaTriggerSelector );
344
345 // Was an iframe lightbox trigger clicked?
346 if( linkTarget ) {
347 if( event.metaKey || event.shiftKey || event.altKey ) {
348 // Let the browser handle meta keys naturally so users can cmd+click
349 return;
350 }
351 const dataPreviewLink = linkTarget.getAttribute( 'data-preview-link' );
352 const dataPreviewLinkIsUrl = typeof dataPreviewLink === 'string' && dataPreviewLink.startsWith( 'http' );
353 let url = dataPreviewLinkIsUrl ? dataPreviewLink : linkTarget.getAttribute( 'href' );
354 if( url ) {
355 this.previewIframe( url );
356 event.preventDefault();
357 }
358 }
359 // Was a media lightbox trigger clicked?
360 else if( mediaTarget ) {
361 if( mediaTarget.hasAttribute( 'data-preview-image' ) ) {
362 let url = mediaTarget.dataset.previewImage || mediaTarget.getAttribute( 'src' );
363 if( url ) {
364 this.previewImage( url, mediaTarget.dataset.previewFit );
365 event.preventDefault();
366 }
367 }
368 else if( mediaTarget.hasAttribute( 'data-preview-video' ) ) {
369 let url = mediaTarget.dataset.previewVideo || mediaTarget.getAttribute( 'src' );
370 if( !url ) {
371 let source = mediaTarget.querySelector( 'source' );
372 if( source ) {
373 url = source.getAttribute( 'src' );
374 }
375 }
376 if( url ) {
377 this.previewVideo( url, mediaTarget.dataset.previewFit );
378 event.preventDefault();
379 }
380 }
381 }
382
383 }
384
385 destroy() {
386
387 this.close();
388
389 }
390
391 }
391 lines JAVASCRIPT