blob: 0a84530a0550d39368b988d35df74011d3ad4c25 [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 */
Marc Kupietz9c036a42024-05-14 13:17:25 +0200177 update( index, fragments, slide = this.Reveal.getCurrentSlide() ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200178
179 let changedFragments = {
180 shown: [],
181 hidden: []
182 };
183
Marc Kupietz9c036a42024-05-14 13:17:25 +0200184 if( slide && this.Reveal.getConfig().fragments ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200185
Marc Kupietz9c036a42024-05-14 13:17:25 +0200186 fragments = fragments || this.sort( slide.querySelectorAll( '.fragment' ) );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200187
188 if( fragments.length ) {
189
190 let maxIndex = 0;
191
192 if( typeof index !== 'number' ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200193 let currentFragment = this.sort( slide.querySelectorAll( '.fragment.visible' ) ).pop();
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200194 if( currentFragment ) {
195 index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
196 }
197 }
198
199 Array.from( fragments ).forEach( ( el, i ) => {
200
201 if( el.hasAttribute( 'data-fragment-index' ) ) {
202 i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
203 }
204
205 maxIndex = Math.max( maxIndex, i );
206
207 // Visible fragments
208 if( i <= index ) {
209 let wasVisible = el.classList.contains( 'visible' )
210 el.classList.add( 'visible' );
211 el.classList.remove( 'current-fragment' );
212
213 if( i === index ) {
214 // Announce the fragments one by one to the Screen Reader
215 this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
216
217 el.classList.add( 'current-fragment' );
218 this.Reveal.slideContent.startEmbeddedContent( el );
219 }
220
221 if( !wasVisible ) {
222 changedFragments.shown.push( el )
223 this.Reveal.dispatchEvent({
224 target: el,
225 type: 'visible',
226 bubbles: false
227 });
228 }
229 }
230 // Hidden fragments
231 else {
232 let wasVisible = el.classList.contains( 'visible' )
233 el.classList.remove( 'visible' );
234 el.classList.remove( 'current-fragment' );
235
236 if( wasVisible ) {
237 this.Reveal.slideContent.stopEmbeddedContent( el );
238 changedFragments.hidden.push( el );
239 this.Reveal.dispatchEvent({
240 target: el,
241 type: 'hidden',
242 bubbles: false
243 });
244 }
245 }
246
247 } );
248
249 // Write the current fragment index to the slide <section>.
250 // This can be used by end users to apply styles based on
251 // the current fragment index.
252 index = typeof index === 'number' ? index : -1;
253 index = Math.max( Math.min( index, maxIndex ), -1 );
Marc Kupietz9c036a42024-05-14 13:17:25 +0200254 slide.setAttribute( 'data-fragment', index );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200255
256 }
257
258 }
259
Marc Kupietz9c036a42024-05-14 13:17:25 +0200260 if( changedFragments.hidden.length ) {
261 this.Reveal.dispatchEvent({
262 type: 'fragmenthidden',
263 data: {
264 fragment: changedFragments.hidden[0],
265 fragments: changedFragments.hidden
266 }
267 });
268 }
269
270 if( changedFragments.shown.length ) {
271 this.Reveal.dispatchEvent({
272 type: 'fragmentshown',
273 data: {
274 fragment: changedFragments.shown[0],
275 fragments: changedFragments.shown
276 }
277 });
278 }
279
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200280 return changedFragments;
281
282 }
283
284 /**
285 * Formats the fragments on the given slide so that they have
286 * valid indices. Call this if fragments are changed in the DOM
287 * after reveal.js has already initialized.
288 *
289 * @param {HTMLElement} slide
290 * @return {Array} a list of the HTML fragments that were synced
291 */
292 sync( slide = this.Reveal.getCurrentSlide() ) {
293
294 return this.sort( slide.querySelectorAll( '.fragment' ) );
295
296 }
297
298 /**
299 * Navigate to the specified slide fragment.
300 *
301 * @param {?number} index The index of the fragment that
302 * should be shown, -1 means all are invisible
303 * @param {number} offset Integer offset to apply to the
304 * fragment index
305 *
306 * @return {boolean} true if a change was made in any
307 * fragments visibility as part of this call
308 */
309 goto( index, offset = 0 ) {
310
311 let currentSlide = this.Reveal.getCurrentSlide();
312 if( currentSlide && this.Reveal.getConfig().fragments ) {
313
314 let fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );
315 if( fragments.length ) {
316
317 // If no index is specified, find the current
318 if( typeof index !== 'number' ) {
319 let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();
320
321 if( lastVisibleFragment ) {
322 index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
323 }
324 else {
325 index = -1;
326 }
327 }
328
329 // Apply the offset if there is one
330 index += offset;
331
332 let changedFragments = this.update( index, fragments );
333
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200334 this.Reveal.controls.update();
335 this.Reveal.progress.update();
336
337 if( this.Reveal.getConfig().fragmentInURL ) {
338 this.Reveal.location.writeURL();
339 }
340
341 return !!( changedFragments.shown.length || changedFragments.hidden.length );
342
343 }
344
345 }
346
347 return false;
348
349 }
350
351 /**
352 * Navigate to the next slide fragment.
353 *
354 * @return {boolean} true if there was a next fragment,
355 * false otherwise
356 */
357 next() {
358
359 return this.goto( null, 1 );
360
361 }
362
363 /**
364 * Navigate to the previous slide fragment.
365 *
366 * @return {boolean} true if there was a previous fragment,
367 * false otherwise
368 */
369 prev() {
370
371 return this.goto( null, -1 );
372
373 }
374
375}