fragments.js 9.2 KB

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