返回 reveal.js
controls.js
根目录 / js / controllers / controls.js
1 import { queryAll, enterFullscreen } from '../utils/util'
2 import { isAndroid } from '../utils/device'
3
4 /**
5 * Manages our presentation controls. This includes both
6 * the built-in control arrows as well as event monitoring
7 * of any elements within the presentation with either of the
8 * following helper classes:
9 * - .navigate-up
10 * - .navigate-right
11 * - .navigate-down
12 * - .navigate-left
13 * - .navigate-next
14 * - .navigate-prev
15 * - .enter-fullscreen
16 */
17 export default class Controls {
18
19 constructor( Reveal ) {
20
21 this.Reveal = Reveal;
22
23 this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );
24 this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );
25 this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );
26 this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
27 this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
28 this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );
29 this.onEnterFullscreen = this.onEnterFullscreen.bind( this );
30
31 }
32
33 render() {
34
35 const rtl = this.Reveal.getConfig().rtl;
36 const revealElement = this.Reveal.getRevealElement();
37
38 this.element = document.createElement( 'aside' );
39 this.element.className = 'controls';
40 this.element.innerHTML =
41 `<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button>
42 <button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button>
43 <button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
44 <button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;
45
46 this.Reveal.getRevealElement().appendChild( this.element );
47
48 // There can be multiple instances of controls throughout the page
49 this.controlsLeft = queryAll( revealElement, '.navigate-left' );
50 this.controlsRight = queryAll( revealElement, '.navigate-right' );
51 this.controlsUp = queryAll( revealElement, '.navigate-up' );
52 this.controlsDown = queryAll( revealElement, '.navigate-down' );
53 this.controlsPrev = queryAll( revealElement, '.navigate-prev' );
54 this.controlsNext = queryAll( revealElement, '.navigate-next' );
55 this.controlsFullscreen = queryAll( revealElement, '.enter-fullscreen' );
56
57 // The left, right and down arrows in the standard reveal.js controls
58 this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
59 this.controlsLeftArrow = this.element.querySelector( '.navigate-left' );
60 this.controlsDownArrow = this.element.querySelector( '.navigate-down' );
61
62 }
63
64 /**
65 * Called when the reveal.js config is updated.
66 */
67 configure( config, oldConfig ) {
68
69 const speakerOnly = config.controls === 'speaker' || config.controls === 'speaker-only';
70
71 this.element.style.display = (
72 config.controls &&
73 (!speakerOnly || this.Reveal.isSpeakerNotes())
74 ) ? 'block' : 'none';
75
76 this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
77 this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );
78
79 }
80
81 bind() {
82
83 // Listen to both touch and click events, in case the device
84 // supports both
85 let pointerEvents = [ 'touchstart', 'click' ];
86
87 // Only support touch for Android, fixes double navigations in
88 // stock browser. Use touchend for it to be considered a valid
89 // user interaction (so we're allowed to autoplay media).
90 if( isAndroid ) {
91 pointerEvents = [ 'touchend' ];
92 }
93
94 pointerEvents.forEach( eventName => {
95 this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
96 this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
97 this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
98 this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
99 this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
100 this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
101 this.controlsFullscreen.forEach( el => el.addEventListener( eventName, this.onEnterFullscreen, false ) );
102 } );
103
104 }
105
106 unbind() {
107
108 [ 'touchstart', 'touchend', 'click' ].forEach( eventName => {
109 this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
110 this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
111 this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
112 this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
113 this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
114 this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
115 this.controlsFullscreen.forEach( el => el.removeEventListener( eventName, this.onEnterFullscreen, false ) );
116 } );
117
118 }
119
120 /**
121 * Updates the state of all control/navigation arrows.
122 */
123 update() {
124
125 let routes = this.Reveal.availableRoutes();
126
127 // Remove the 'enabled' class from all directions
128 [...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
129 node.classList.remove( 'enabled', 'fragmented' );
130
131 // Set 'disabled' attribute on all directions
132 node.setAttribute( 'disabled', 'disabled' );
133 } );
134
135 // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
136 if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
137 if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
138 if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
139 if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
140
141 // Prev/next buttons
142 if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
143 if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
144
145 // Highlight fragment directions
146 let currentSlide = this.Reveal.getCurrentSlide();
147 if( currentSlide ) {
148
149 let fragmentsRoutes = this.Reveal.fragments.availableRoutes();
150
151 // Always apply fragment decorator to prev/next buttons
152 if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
153 if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
154
155 const isVerticalStack = this.Reveal.isVerticalSlide( currentSlide );
156 const hasVerticalSiblings = isVerticalStack &&
157 currentSlide.parentElement &&
158 currentSlide.parentElement.querySelectorAll( ':scope > section' ).length > 1;
159
160 // Apply fragment decorators to directional buttons based on
161 // what slide axis they are in
162 if( isVerticalStack && hasVerticalSiblings ) {
163 if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
164 if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
165 }
166 else {
167 if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
168 if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
169 }
170
171 }
172
173 if( this.Reveal.getConfig().controlsTutorial ) {
174
175 let indices = this.Reveal.getIndices();
176
177 // Highlight control arrows with an animation to ensure
178 // that the viewer knows how to navigate
179 if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
180 this.controlsDownArrow.classList.add( 'highlight' );
181 }
182 else {
183 this.controlsDownArrow.classList.remove( 'highlight' );
184
185 if( this.Reveal.getConfig().rtl ) {
186
187 if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
188 this.controlsLeftArrow.classList.add( 'highlight' );
189 }
190 else {
191 this.controlsLeftArrow.classList.remove( 'highlight' );
192 }
193
194 } else {
195
196 if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
197 this.controlsRightArrow.classList.add( 'highlight' );
198 }
199 else {
200 this.controlsRightArrow.classList.remove( 'highlight' );
201 }
202 }
203 }
204 }
205 }
206
207 destroy() {
208
209 this.unbind();
210 this.element.remove();
211
212 }
213
214 /**
215 * Event handlers for navigation control buttons.
216 */
217 onNavigateLeftClicked( event ) {
218
219 event.preventDefault();
220 this.Reveal.onUserInput();
221
222 if( this.Reveal.getConfig().navigationMode === 'linear' ) {
223 this.Reveal.prev();
224 }
225 else {
226 this.Reveal.left();
227 }
228
229 }
230
231 onNavigateRightClicked( event ) {
232
233 event.preventDefault();
234 this.Reveal.onUserInput();
235
236 if( this.Reveal.getConfig().navigationMode === 'linear' ) {
237 this.Reveal.next();
238 }
239 else {
240 this.Reveal.right();
241 }
242
243 }
244
245 onNavigateUpClicked( event ) {
246
247 event.preventDefault();
248 this.Reveal.onUserInput();
249
250 this.Reveal.up();
251
252 }
253
254 onNavigateDownClicked( event ) {
255
256 event.preventDefault();
257 this.Reveal.onUserInput();
258
259 this.Reveal.down();
260
261 }
262
263 onNavigatePrevClicked( event ) {
264
265 event.preventDefault();
266 this.Reveal.onUserInput();
267
268 this.Reveal.prev();
269
270 }
271
272 onNavigateNextClicked( event ) {
273
274 event.preventDefault();
275 this.Reveal.onUserInput();
276
277 this.Reveal.next();
278
279 }
280
281 onEnterFullscreen( event ) {
282
283 const config = this.Reveal.getConfig();
284 const viewport = this.Reveal.getViewportElement();
285
286 enterFullscreen( config.embedded ? viewport : viewport.parentElement );
287
288 }
289
290 }
290 lines JAVASCRIPT