blob: 796c1684661823d30d302298f81ab6e6c7d4c5ae [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import { extend, queryAll } from '../utils/util.js'
2
3/**
4 * Handles sorting and navigation of slide fragments.
5 * Fragments are elements within a slide that are
6 * revealed/animated incrementally.
7 */
8export default class Fragments {
9
10 constructor( Reveal ) {
11
12 this.Reveal = Reveal;
13
14 }
15
16 /**
17 * Called when the reveal.js config is updated.
18 */
19 configure( config, oldConfig ) {
20
21 if( config.fragments === false ) {
22 this.disable();
23 }
24 else if( oldConfig.fragments === false ) {
25 this.enable();
26 }
27
28 }
29
30 /**
31 * If fragments are disabled in the deck, they should all be
32 * visible rather than stepped through.
33 */
34 disable() {
35
36 queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
37 element.classList.add( 'visible' );
38 element.classList.remove( 'current-fragment' );
39 } );
40
41 }
42
43 /**
44 * Reverse of #disable(). Only called if fragments have
45 * previously been disabled.
46 */
47 enable() {
48
49 queryAll( this.Reveal.getSlidesElement(), '.fragment' ).forEach( element => {
50 element.classList.remove( 'visible' );
51 element.classList.remove( 'current-fragment' );
52 } );
53
54 }
55
56 /**
57 * Returns an object describing the available fragment
58 * directions.
59 *
60 * @return {{prev: boolean, next: boolean}}
61 */
62 availableRoutes() {
63
64 let currentSlide = this.Reveal.getCurrentSlide();
65 if( currentSlide && this.Reveal.getConfig().fragments ) {
66 let fragments = currentSlide.querySelectorAll( '.fragment:not(.disabled)' );
67 let hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.disabled):not(.visible)' );
68
69 return {
70 prev: fragments.length - hiddenFragments.length > 0,
71 next: !!hiddenFragments.length
72 };
73 }
74 else {
75 return { prev: false, next: false };
76 }
77
78 }
79
80 /**
81 * Return a sorted fragments list, ordered by an increasing
82 * "data-fragment-index" attribute.
83 *
84 * Fragments will be revealed in the order that they are returned by
85 * this function, so you can use the index attributes to control the
86 * order of fragment appearance.
87 *
88 * To maintain a sensible default fragment order, fragments are presumed
89 * to be passed in document order. This function adds a "fragment-index"
90 * attribute to each node if such an attribute is not already present,
91 * and sets that attribute to an integer value which is the position of
92 * the fragment within the fragments list.
93 *
94 * @param {object[]|*} fragments
95 * @param {boolean} grouped If true the returned array will contain
96 * nested arrays for all fragments with the same index
97 * @return {object[]} sorted Sorted array of fragments
98 */
99 sort( fragments, grouped = false ) {
100
101 fragments = Array.from( fragments );
102
103 let ordered = [],
104 unordered = [],
105 sorted = [];
106
107 // Group ordered and unordered elements
108 fragments.forEach( fragment => {
109 if( fragment.hasAttribute( 'data-fragment-index' ) ) {
110 let index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
111
112 if( !ordered[index] ) {
113 ordered[index] = [];
114 }
115
116 ordered[index].push( fragment );
117 }
118 else {
119 unordered.push( [ fragment ] );
120 }
121 } );
122
123 // Append fragments without explicit indices in their
124 // DOM order
125 ordered = ordered.concat( unordered );
126
127 // Manually count the index up per group to ensure there
128 // are no gaps
129 let index = 0;
130
131 // Push all fragments in their sorted order to an array,
132 // this flattens the groups
133 ordered.forEach( group => {
134 group.forEach( fragment => {
135 sorted.push( fragment );
136 fragment.setAttribute( 'data-fragment-index', index );
137 } );
138
139 index ++;
140 } );
141
142 return grouped === true ? ordered : sorted;
143
144 }
145
146 /**
147 * Sorts and formats all of fragments in the
148 * presentation.
149 */
150 sortAll() {
151
152 this.Reveal.getHorizontalSlides().forEach( horizontalSlide => {
153
154 let verticalSlides = queryAll( horizontalSlide, 'section' );
155 verticalSlides.forEach( ( verticalSlide, y ) => {
156
157 this.sort( verticalSlide.querySelectorAll( '.fragment' ) );
158
159 }, this );
160
161 if( verticalSlides.length === 0 ) this.sort( horizontalSlide.querySelectorAll( '.fragment' ) );
162
163 } );
164
165 }
166
167 /**
168 * Refreshes the fragments on the current slide so that they
169 * have the appropriate classes (.visible + .current-fragment).
170 *
171 * @param {number} [index] The index of the current fragment
172 * @param {array} [fragments] Array containing all fragments
173 * in the current slide
174 *
175 * @return {{shown: array, hidden: array}}
176 */
177 update( index, fragments ) {
178
179 let changedFragments = {
180 shown: [],
181 hidden: []
182 };
183
184 let currentSlide = this.Reveal.getCurrentSlide();
185 if( currentSlide && this.Reveal.getConfig().fragments ) {
186
187 fragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) );
188
189 if( fragments.length ) {
190
191 let maxIndex = 0;
192
193 if( typeof index !== 'number' ) {
194 let currentFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
195 if( currentFragment ) {
196 index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
197 }
198 }
199
200 Array.from( fragments ).forEach( ( el, i ) => {
201
202 if( el.hasAttribute( 'data-fragment-index' ) ) {
203 i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
204 }
205
206 maxIndex = Math.max( maxIndex, i );
207
208 // Visible fragments
209 if( i <= index ) {
210 let wasVisible = el.classList.contains( 'visible' )
211 el.classList.add( 'visible' );
212 el.classList.remove( 'current-fragment' );
213
214 if( i === index ) {
215 // Announce the fragments one by one to the Screen Reader
216 this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
217
218 el.classList.add( 'current-fragment' );
219 this.Reveal.slideContent.startEmbeddedContent( el );
220 }
221
222 if( !wasVisible ) {
223 changedFragments.shown.push( el )
224 this.Reveal.dispatchEvent({
225 target: el,
226 type: 'visible',
227 bubbles: false
228 });
229 }
230 }
231 // Hidden fragments
232 else {
233 let wasVisible = el.classList.contains( 'visible' )
234 el.classList.remove( 'visible' );
235 el.classList.remove( 'current-fragment' );
236
237 if( wasVisible ) {
238 this.Reveal.slideContent.stopEmbeddedContent( el );
239 changedFragments.hidden.push( el );
240 this.Reveal.dispatchEvent({
241 target: el,
242 type: 'hidden',
243 bubbles: false
244 });
245 }
246 }
247
248 } );
249
250 // Write the current fragment index to the slide <section>.
251 // This can be used by end users to apply styles based on
252 // the current fragment index.
253 index = typeof index === 'number' ? index : -1;
254 index = Math.max( Math.min( index, maxIndex ), -1 );
255 currentSlide.setAttribute( 'data-fragment', index );
256
257 }
258
259 }
260
261 return changedFragments;
262
263 }
264
265 /**
266 * Formats the fragments on the given slide so that they have
267 * valid indices. Call this if fragments are changed in the DOM
268 * after reveal.js has already initialized.
269 *
270 * @param {HTMLElement} slide
271 * @return {Array} a list of the HTML fragments that were synced
272 */
273 sync( slide = this.Reveal.getCurrentSlide() ) {
274
275 return this.sort( slide.querySelectorAll( '.fragment' ) );
276
277 }
278
279 /**
280 * Navigate to the specified slide fragment.
281 *
282 * @param {?number} index The index of the fragment that
283 * should be shown, -1 means all are invisible
284 * @param {number} offset Integer offset to apply to the
285 * fragment index
286 *
287 * @return {boolean} true if a change was made in any
288 * fragments visibility as part of this call
289 */
290 goto( index, offset = 0 ) {
291
292 let currentSlide = this.Reveal.getCurrentSlide();
293 if( currentSlide && this.Reveal.getConfig().fragments ) {
294
295 let fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );
296 if( fragments.length ) {
297
298 // If no index is specified, find the current
299 if( typeof index !== 'number' ) {
300 let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();
301
302 if( lastVisibleFragment ) {
303 index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
304 }
305 else {
306 index = -1;
307 }
308 }
309
310 // Apply the offset if there is one
311 index += offset;
312
313 let changedFragments = this.update( index, fragments );
314
315 if( changedFragments.hidden.length ) {
316 this.Reveal.dispatchEvent({
317 type: 'fragmenthidden',
318 data: {
319 fragment: changedFragments.hidden[0],
320 fragments: changedFragments.hidden
321 }
322 });
323 }
324
325 if( changedFragments.shown.length ) {
326 this.Reveal.dispatchEvent({
327 type: 'fragmentshown',
328 data: {
329 fragment: changedFragments.shown[0],
330 fragments: changedFragments.shown
331 }
332 });
333 }
334
335 this.Reveal.controls.update();
336 this.Reveal.progress.update();
337
338 if( this.Reveal.getConfig().fragmentInURL ) {
339 this.Reveal.location.writeURL();
340 }
341
342 return !!( changedFragments.shown.length || changedFragments.hidden.length );
343
344 }
345
346 }
347
348 return false;
349
350 }
351
352 /**
353 * Navigate to the next slide fragment.
354 *
355 * @return {boolean} true if there was a next fragment,
356 * false otherwise
357 */
358 next() {
359
360 return this.goto( null, 1 );
361
362 }
363
364 /**
365 * Navigate to the previous slide fragment.
366 *
367 * @return {boolean} true if there was a previous fragment,
368 * false otherwise
369 */
370 prev() {
371
372 return this.goto( null, -1 );
373
374 }
375
376}