Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c09ce88

Browse files
complanarhenrikingo
authored andcommittedMar 17, 2018
New plugin: media (impress#676)
The media plugin can autoplay and autopause/autostop <audio> and <video> elements when entering and leaving a step. Support for impressConsole: don't autoplay in preview window and play but mute clips in current window.
1 parent 09b5d46 commit c09ce88

File tree

5 files changed

+503
-2
lines changed

5 files changed

+503
-2
lines changed
 

‎.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
/node_modules
33
/npm-debug.log
44
/*.tgz
5+
6+
# Files for editors and other tools
7+
/.brackets.json

‎build.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ buildify()
1717
'src/plugins/goto/goto.js',
1818
'src/plugins/help/help.js',
1919
'src/plugins/impressConsole/impressConsole.js',
20+
'src/plugins/media/media.js',
2021
'src/plugins/mobile/mobile.js',
2122
'src/plugins/mouse-timeout/mouse-timeout.js',
2223
'src/plugins/navigation/navigation.js',

‎js/impress.js

Lines changed: 250 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2510,7 +2510,7 @@
25102510
//Btw, you can also launch console automatically:
25112511
//<div id="impress" data-console-autolaunch="true">
25122512
if ( root.dataset.consoleAutolaunch === 'true' ) {
2513-
window.open();
2513+
open();
25142514
}
25152515
};
25162516

@@ -2706,6 +2706,255 @@
27062706

27072707
} )( document, window );
27082708

2709+
/**
2710+
* Media Plugin
2711+
*
2712+
* This plugin will do the following things:
2713+
*
2714+
* - Add a special class when playing (body.impress-media-video-playing
2715+
* and body.impress-media-video-playing) and pausing media (body.impress-media-video-paused
2716+
* and body.impress-media-audio-paused) (removing them when ending).
2717+
* This can be useful for example for darkening the background or fading out other elements
2718+
* while a video is playing.
2719+
* Only media at the current step are taken into account. All classes are removed when leaving
2720+
* a step.
2721+
*
2722+
* - Introduce the following new data attributes:
2723+
*
2724+
* - data-media-autoplay="true": Autostart media when entering its step.
2725+
* - data-media-autostop="true": Stop media (= pause and reset to start), when leaving its
2726+
* step.
2727+
* - data-media-autopause="true": Pause media but keep current time when leaving its step.
2728+
*
2729+
* When these attributes are added to a step they are inherited by all media on this step.
2730+
* Of course this setting can be overwritten by adding different attributes to inidvidual
2731+
* media.
2732+
*
2733+
* The same rule applies when this attributes is added to the root element. Settings can be
2734+
* overwritten for individual steps and media.
2735+
*
2736+
* Examples:
2737+
* - data-media-autostart="true" data-media-autostop="true": start media on enter, stop on
2738+
* leave, restart from beginning when re-entering the step.
2739+
*
2740+
* - data-media-autostart="true" data-media-autopause="true": start media on enter, pause on
2741+
* leave, resume on re-enter
2742+
*
2743+
* - data-media-autostart="true" data-media-autostop="true" data-media-autopause="true": start
2744+
* media on enter, stop on leave (stop overwrites pause).
2745+
*
2746+
* - data-media-autostart="true" data-media-autopause="false": let media start automatically
2747+
* when entering a step and let it play when leaving the step.
2748+
*
2749+
* - <div id="impress" data-media-autostart="true"> ... <div class="step"
2750+
* data-media-autostart="false">
2751+
* All media is startet automatically on all steps except the one that has the
2752+
* data-media-autostart="false" attribute.
2753+
*
2754+
* - Pro tip: Use <audio onended="impress().next()"> or <video onended="impress().next()"> to
2755+
* proceed to the next step automatically, when the end of the media is reached.
2756+
*
2757+
*
2758+
* Copyright 2018 Holger Teichert (@complanar)
2759+
* Released under the MIT license.
2760+
*/
2761+
/* global window, document */
2762+
2763+
( function( document, window ) {
2764+
"use strict";
2765+
var root, api, gc, attributeTracker;
2766+
2767+
attributeTracker = [];
2768+
2769+
// Function names
2770+
var enhanceMediaNodes,
2771+
enhanceMedia,
2772+
removeMediaClasses,
2773+
onStepenterDetectImpressConsole,
2774+
onStepenter,
2775+
onStepleave,
2776+
onPlay,
2777+
onPause,
2778+
onEnded,
2779+
getMediaAttribute,
2780+
teardown;
2781+
2782+
document.addEventListener( "impress:init", function( event ) {
2783+
root = event.target;
2784+
api = event.detail.api;
2785+
gc = api.lib.gc;
2786+
2787+
enhanceMedia();
2788+
2789+
gc.pushCallback( teardown );
2790+
}, false );
2791+
2792+
teardown = function() {
2793+
var el, i;
2794+
removeMediaClasses();
2795+
for ( i = 0; i < attributeTracker.length; i += 1 ) {
2796+
el = attributeTracker[ i ];
2797+
el.node.removeAttribute( el.attr );
2798+
}
2799+
attributeTracker = [];
2800+
};
2801+
2802+
getMediaAttribute = function( attributeName, nodes ) {
2803+
var attrName, attrValue, i, node;
2804+
attrName = "data-media-" + attributeName;
2805+
2806+
// Look for attributes in all nodes
2807+
for ( i = 0; i < nodes.length; i += 1 ) {
2808+
node = nodes[ i ];
2809+
2810+
// First test, if the attribute exists, because some browsers may return
2811+
// an empty string for non-existing attributes - specs are not clear at that point
2812+
if ( node.hasAttribute( attrName ) ) {
2813+
2814+
// Attribute found, return their parsed boolean value, empty strings count as true
2815+
// to enable empty value booleans (common in html5 but not allowed in well formed
2816+
// xml).
2817+
attrValue = node.getAttribute( attrName );
2818+
if ( attrValue === "" || attrValue === "true" ) {
2819+
return true;
2820+
} else {
2821+
return false;
2822+
}
2823+
}
2824+
2825+
// No attribute found at current node, proceed with next round
2826+
}
2827+
2828+
// Last resort: no attribute found - return undefined to distiguish from false
2829+
return undefined;
2830+
};
2831+
2832+
onPlay = function( event ) {
2833+
var type = event.target.nodeName.toLowerCase();
2834+
document.body.classList.add( "impress-media-" + type + "-playing" );
2835+
document.body.classList.remove( "impress-media-" + type + "-paused" );
2836+
};
2837+
2838+
onPause = function( event ) {
2839+
var type = event.target.nodeName.toLowerCase();
2840+
document.body.classList.add( "impress-media-" + type + "-paused" );
2841+
document.body.classList.remove( "impress-media-" + type + "-playing" );
2842+
};
2843+
2844+
onEnded = function( event ) {
2845+
var type = event.target.nodeName.toLowerCase();
2846+
document.body.classList.remove( "impress-media-" + type + "-playing" );
2847+
document.body.classList.remove( "impress-media-" + type + "-paused" );
2848+
};
2849+
2850+
removeMediaClasses = function() {
2851+
var type, types;
2852+
types = [ "video", "audio" ];
2853+
for ( type in types ) {
2854+
document.body.classList.remove( "impress-media-" + types[ type ] + "-playing" );
2855+
document.body.classList.remove( "impress-media-" + types[ type ] + "-paused" );
2856+
}
2857+
};
2858+
2859+
enhanceMediaNodes = function() {
2860+
var i, id, media, mediaElement, type;
2861+
2862+
media = root.querySelectorAll( "audio, video" );
2863+
for ( i = 0; i < media.length; i += 1 ) {
2864+
type = media[ i ].nodeName.toLowerCase();
2865+
2866+
// Set an id to identify each media node - used e.g. for cross references by
2867+
// the consoleMedia plugin
2868+
mediaElement = media[ i ];
2869+
id = mediaElement.getAttribute( "id" );
2870+
if ( id === undefined || id === null ) {
2871+
mediaElement.setAttribute( "id", "media-" + type + "-" + i );
2872+
attributeTracker.push( { "node": mediaElement, "attr": "id" } );
2873+
}
2874+
gc.addEventListener( mediaElement, "play", onPlay );
2875+
gc.addEventListener( mediaElement, "playing", onPlay );
2876+
gc.addEventListener( mediaElement, "pause", onPause );
2877+
gc.addEventListener( mediaElement, "ended", onEnded );
2878+
}
2879+
};
2880+
2881+
enhanceMedia = function() {
2882+
var steps, stepElement, i;
2883+
enhanceMediaNodes();
2884+
steps = document.getElementsByClassName( "step" );
2885+
for ( i = 0; i < steps.length; i += 1 ) {
2886+
stepElement = steps[ i ];
2887+
2888+
gc.addEventListener( stepElement, "impress:stepenter", onStepenter );
2889+
gc.addEventListener( stepElement, "impress:stepleave", onStepleave );
2890+
}
2891+
};
2892+
2893+
onStepenterDetectImpressConsole = function() {
2894+
return {
2895+
"preview": ( window.frameElement !== null && window.frameElement.id === "preView" ),
2896+
"slideView": ( window.frameElement !== null && window.frameElement.id === "slideView" )
2897+
};
2898+
};
2899+
2900+
onStepenter = function( event ) {
2901+
var stepElement, media, mediaElement, i, onConsole, autoplay;
2902+
if ( ( !event ) || ( !event.target ) ) {
2903+
return;
2904+
}
2905+
2906+
stepElement = event.target;
2907+
removeMediaClasses();
2908+
2909+
media = stepElement.querySelectorAll( "audio, video" );
2910+
for ( i = 0; i < media.length; i += 1 ) {
2911+
mediaElement = media[ i ];
2912+
2913+
// Autoplay when (maybe inherited) autoplay setting is true,
2914+
// but only if not on preview of the next step in impressConsole
2915+
onConsole = onStepenterDetectImpressConsole();
2916+
autoplay = getMediaAttribute( "autoplay", [ mediaElement, stepElement, root ] );
2917+
if ( autoplay && !onConsole.preview ) {
2918+
if ( onConsole.slideView ) {
2919+
mediaElement.muted = true;
2920+
}
2921+
mediaElement.play();
2922+
}
2923+
}
2924+
};
2925+
2926+
onStepleave = function( event ) {
2927+
var stepElement, media, i, mediaElement, autoplay, autopause, autostop;
2928+
if ( ( !event || !event.target ) ) {
2929+
return;
2930+
}
2931+
2932+
stepElement = event.target;
2933+
media = event.target.querySelectorAll( "audio, video" );
2934+
for ( i = 0; i < media.length; i += 1 ) {
2935+
mediaElement = media[ i ];
2936+
2937+
autoplay = getMediaAttribute( "autoplay", [ mediaElement, stepElement, root ] );
2938+
autopause = getMediaAttribute( "autopause", [ mediaElement, stepElement, root ] );
2939+
autostop = getMediaAttribute( "autostop", [ mediaElement, stepElement, root ] );
2940+
2941+
// If both autostop and autopause are undefined, set it to the value of autoplay
2942+
if ( autostop === undefined && autopause === undefined ) {
2943+
autostop = autoplay;
2944+
}
2945+
2946+
if ( autopause || autostop ) {
2947+
mediaElement.pause();
2948+
if ( autostop ) {
2949+
mediaElement.currentTime = 0;
2950+
}
2951+
}
2952+
}
2953+
removeMediaClasses();
2954+
};
2955+
2956+
} )( document, window );
2957+
27092958
/**
27102959
* Mobile devices support
27112960
*

‎src/plugins/impressConsole/impressConsole.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@
555555
//Btw, you can also launch console automatically:
556556
//<div id="impress" data-console-autolaunch="true">
557557
if ( root.dataset.consoleAutolaunch === 'true' ) {
558-
window.open();
558+
open();
559559
}
560560
};
561561

‎src/plugins/media/media.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/**
2+
* Media Plugin
3+
*
4+
* This plugin will do the following things:
5+
*
6+
* - Add a special class when playing (body.impress-media-video-playing
7+
* and body.impress-media-video-playing) and pausing media (body.impress-media-video-paused
8+
* and body.impress-media-audio-paused) (removing them when ending).
9+
* This can be useful for example for darkening the background or fading out other elements
10+
* while a video is playing.
11+
* Only media at the current step are taken into account. All classes are removed when leaving
12+
* a step.
13+
*
14+
* - Introduce the following new data attributes:
15+
*
16+
* - data-media-autoplay="true": Autostart media when entering its step.
17+
* - data-media-autostop="true": Stop media (= pause and reset to start), when leaving its
18+
* step.
19+
* - data-media-autopause="true": Pause media but keep current time when leaving its step.
20+
*
21+
* When these attributes are added to a step they are inherited by all media on this step.
22+
* Of course this setting can be overwritten by adding different attributes to inidvidual
23+
* media.
24+
*
25+
* The same rule applies when this attributes is added to the root element. Settings can be
26+
* overwritten for individual steps and media.
27+
*
28+
* Examples:
29+
* - data-media-autostart="true" data-media-autostop="true": start media on enter, stop on
30+
* leave, restart from beginning when re-entering the step.
31+
*
32+
* - data-media-autostart="true" data-media-autopause="true": start media on enter, pause on
33+
* leave, resume on re-enter
34+
*
35+
* - data-media-autostart="true" data-media-autostop="true" data-media-autopause="true": start
36+
* media on enter, stop on leave (stop overwrites pause).
37+
*
38+
* - data-media-autostart="true" data-media-autopause="false": let media start automatically
39+
* when entering a step and let it play when leaving the step.
40+
*
41+
* - <div id="impress" data-media-autostart="true"> ... <div class="step"
42+
* data-media-autostart="false">
43+
* All media is startet automatically on all steps except the one that has the
44+
* data-media-autostart="false" attribute.
45+
*
46+
* - Pro tip: Use <audio onended="impress().next()"> or <video onended="impress().next()"> to
47+
* proceed to the next step automatically, when the end of the media is reached.
48+
*
49+
*
50+
* Copyright 2018 Holger Teichert (@complanar)
51+
* Released under the MIT license.
52+
*/
53+
/* global window, document */
54+
55+
( function( document, window ) {
56+
"use strict";
57+
var root, api, gc, attributeTracker;
58+
59+
attributeTracker = [];
60+
61+
// Function names
62+
var enhanceMediaNodes,
63+
enhanceMedia,
64+
removeMediaClasses,
65+
onStepenterDetectImpressConsole,
66+
onStepenter,
67+
onStepleave,
68+
onPlay,
69+
onPause,
70+
onEnded,
71+
getMediaAttribute,
72+
teardown;
73+
74+
document.addEventListener( "impress:init", function( event ) {
75+
root = event.target;
76+
api = event.detail.api;
77+
gc = api.lib.gc;
78+
79+
enhanceMedia();
80+
81+
gc.pushCallback( teardown );
82+
}, false );
83+
84+
teardown = function() {
85+
var el, i;
86+
removeMediaClasses();
87+
for ( i = 0; i < attributeTracker.length; i += 1 ) {
88+
el = attributeTracker[ i ];
89+
el.node.removeAttribute( el.attr );
90+
}
91+
attributeTracker = [];
92+
};
93+
94+
getMediaAttribute = function( attributeName, nodes ) {
95+
var attrName, attrValue, i, node;
96+
attrName = "data-media-" + attributeName;
97+
98+
// Look for attributes in all nodes
99+
for ( i = 0; i < nodes.length; i += 1 ) {
100+
node = nodes[ i ];
101+
102+
// First test, if the attribute exists, because some browsers may return
103+
// an empty string for non-existing attributes - specs are not clear at that point
104+
if ( node.hasAttribute( attrName ) ) {
105+
106+
// Attribute found, return their parsed boolean value, empty strings count as true
107+
// to enable empty value booleans (common in html5 but not allowed in well formed
108+
// xml).
109+
attrValue = node.getAttribute( attrName );
110+
if ( attrValue === "" || attrValue === "true" ) {
111+
return true;
112+
} else {
113+
return false;
114+
}
115+
}
116+
117+
// No attribute found at current node, proceed with next round
118+
}
119+
120+
// Last resort: no attribute found - return undefined to distiguish from false
121+
return undefined;
122+
};
123+
124+
onPlay = function( event ) {
125+
var type = event.target.nodeName.toLowerCase();
126+
document.body.classList.add( "impress-media-" + type + "-playing" );
127+
document.body.classList.remove( "impress-media-" + type + "-paused" );
128+
};
129+
130+
onPause = function( event ) {
131+
var type = event.target.nodeName.toLowerCase();
132+
document.body.classList.add( "impress-media-" + type + "-paused" );
133+
document.body.classList.remove( "impress-media-" + type + "-playing" );
134+
};
135+
136+
onEnded = function( event ) {
137+
var type = event.target.nodeName.toLowerCase();
138+
document.body.classList.remove( "impress-media-" + type + "-playing" );
139+
document.body.classList.remove( "impress-media-" + type + "-paused" );
140+
};
141+
142+
removeMediaClasses = function() {
143+
var type, types;
144+
types = [ "video", "audio" ];
145+
for ( type in types ) {
146+
document.body.classList.remove( "impress-media-" + types[ type ] + "-playing" );
147+
document.body.classList.remove( "impress-media-" + types[ type ] + "-paused" );
148+
}
149+
};
150+
151+
enhanceMediaNodes = function() {
152+
var i, id, media, mediaElement, type;
153+
154+
media = root.querySelectorAll( "audio, video" );
155+
for ( i = 0; i < media.length; i += 1 ) {
156+
type = media[ i ].nodeName.toLowerCase();
157+
158+
// Set an id to identify each media node - used e.g. for cross references by
159+
// the consoleMedia plugin
160+
mediaElement = media[ i ];
161+
id = mediaElement.getAttribute( "id" );
162+
if ( id === undefined || id === null ) {
163+
mediaElement.setAttribute( "id", "media-" + type + "-" + i );
164+
attributeTracker.push( { "node": mediaElement, "attr": "id" } );
165+
}
166+
gc.addEventListener( mediaElement, "play", onPlay );
167+
gc.addEventListener( mediaElement, "playing", onPlay );
168+
gc.addEventListener( mediaElement, "pause", onPause );
169+
gc.addEventListener( mediaElement, "ended", onEnded );
170+
}
171+
};
172+
173+
enhanceMedia = function() {
174+
var steps, stepElement, i;
175+
enhanceMediaNodes();
176+
steps = document.getElementsByClassName( "step" );
177+
for ( i = 0; i < steps.length; i += 1 ) {
178+
stepElement = steps[ i ];
179+
180+
gc.addEventListener( stepElement, "impress:stepenter", onStepenter );
181+
gc.addEventListener( stepElement, "impress:stepleave", onStepleave );
182+
}
183+
};
184+
185+
onStepenterDetectImpressConsole = function() {
186+
return {
187+
"preview": ( window.frameElement !== null && window.frameElement.id === "preView" ),
188+
"slideView": ( window.frameElement !== null && window.frameElement.id === "slideView" )
189+
};
190+
};
191+
192+
onStepenter = function( event ) {
193+
var stepElement, media, mediaElement, i, onConsole, autoplay;
194+
if ( ( !event ) || ( !event.target ) ) {
195+
return;
196+
}
197+
198+
stepElement = event.target;
199+
removeMediaClasses();
200+
201+
media = stepElement.querySelectorAll( "audio, video" );
202+
for ( i = 0; i < media.length; i += 1 ) {
203+
mediaElement = media[ i ];
204+
205+
// Autoplay when (maybe inherited) autoplay setting is true,
206+
// but only if not on preview of the next step in impressConsole
207+
onConsole = onStepenterDetectImpressConsole();
208+
autoplay = getMediaAttribute( "autoplay", [ mediaElement, stepElement, root ] );
209+
if ( autoplay && !onConsole.preview ) {
210+
if ( onConsole.slideView ) {
211+
mediaElement.muted = true;
212+
}
213+
mediaElement.play();
214+
}
215+
}
216+
};
217+
218+
onStepleave = function( event ) {
219+
var stepElement, media, i, mediaElement, autoplay, autopause, autostop;
220+
if ( ( !event || !event.target ) ) {
221+
return;
222+
}
223+
224+
stepElement = event.target;
225+
media = event.target.querySelectorAll( "audio, video" );
226+
for ( i = 0; i < media.length; i += 1 ) {
227+
mediaElement = media[ i ];
228+
229+
autoplay = getMediaAttribute( "autoplay", [ mediaElement, stepElement, root ] );
230+
autopause = getMediaAttribute( "autopause", [ mediaElement, stepElement, root ] );
231+
autostop = getMediaAttribute( "autostop", [ mediaElement, stepElement, root ] );
232+
233+
// If both autostop and autopause are undefined, set it to the value of autoplay
234+
if ( autostop === undefined && autopause === undefined ) {
235+
autostop = autoplay;
236+
}
237+
238+
if ( autopause || autostop ) {
239+
mediaElement.pause();
240+
if ( autostop ) {
241+
mediaElement.currentTime = 0;
242+
}
243+
}
244+
}
245+
removeMediaClasses();
246+
};
247+
248+
} )( document, window );

0 commit comments

Comments
 (0)
Please sign in to comment.