1
0

fragments.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { extend, toArray } 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. * Shows all fragments in the presentation. Used when
  13. * fragments are disabled presentation-wide.
  14. */
  15. showAll() {
  16. toArray( this.Reveal.getSlidesElement().querySelectorAll( '.fragment' ) ).forEach( element => {
  17. element.classList.add( 'visible' );
  18. element.classList.remove( 'current-fragment' );
  19. } );
  20. }
  21. /**
  22. * Returns an object describing the available fragment
  23. * directions.
  24. *
  25. * @return {{prev: boolean, next: boolean}}
  26. */
  27. availableRoutes() {
  28. let currentSlide = this.Reveal.getCurrentSlide();
  29. if( currentSlide && this.Reveal.getConfig().fragments ) {
  30. let fragments = currentSlide.querySelectorAll( '.fragment' );
  31. let hiddenFragments = currentSlide.querySelectorAll( '.fragment:not(.visible)' );
  32. return {
  33. prev: fragments.length - hiddenFragments.length > 0,
  34. next: !!hiddenFragments.length
  35. };
  36. }
  37. else {
  38. return { prev: false, next: false };
  39. }
  40. }
  41. /**
  42. * Return a sorted fragments list, ordered by an increasing
  43. * "data-fragment-index" attribute.
  44. *
  45. * Fragments will be revealed in the order that they are returned by
  46. * this function, so you can use the index attributes to control the
  47. * order of fragment appearance.
  48. *
  49. * To maintain a sensible default fragment order, fragments are presumed
  50. * to be passed in document order. This function adds a "fragment-index"
  51. * attribute to each node if such an attribute is not already present,
  52. * and sets that attribute to an integer value which is the position of
  53. * the fragment within the fragments list.
  54. *
  55. * @param {object[]|*} fragments
  56. * @param {boolean} grouped If true the returned array will contain
  57. * nested arrays for all fragments with the same index
  58. * @return {object[]} sorted Sorted array of fragments
  59. */
  60. sort( fragments, grouped = false ) {
  61. fragments = toArray( fragments );
  62. let ordered = [],
  63. unordered = [],
  64. sorted = [];
  65. // Group ordered and unordered elements
  66. fragments.forEach( fragment => {
  67. if( fragment.hasAttribute( 'data-fragment-index' ) ) {
  68. let index = parseInt( fragment.getAttribute( 'data-fragment-index' ), 10 );
  69. if( !ordered[index] ) {
  70. ordered[index] = [];
  71. }
  72. ordered[index].push( fragment );
  73. }
  74. else {
  75. unordered.push( [ fragment ] );
  76. }
  77. } );
  78. // Append fragments without explicit indices in their
  79. // DOM order
  80. ordered = ordered.concat( unordered );
  81. // Manually count the index up per group to ensure there
  82. // are no gaps
  83. let index = 0;
  84. // Push all fragments in their sorted order to an array,
  85. // this flattens the groups
  86. ordered.forEach( group => {
  87. group.forEach( fragment => {
  88. sorted.push( fragment );
  89. fragment.setAttribute( 'data-fragment-index', index );
  90. } );
  91. index ++;
  92. } );
  93. return grouped === true ? ordered : sorted;
  94. }
  95. /**
  96. * Sorts and formats all of fragments in the
  97. * presentation.
  98. */
  99. sortAll() {
  100. this.Reveal.getHorizontalSlides().forEach( horizontalSlide => {
  101. let verticalSlides = toArray( horizontalSlide.querySelectorAll( 'section' ) );
  102. verticalSlides.forEach( ( verticalSlide, y ) => {
  103. this.sort( verticalSlide.querySelectorAll( '.fragment' ) );
  104. }, this );
  105. if( verticalSlides.length === 0 ) this.sort( horizontalSlide.querySelectorAll( '.fragment' ) );
  106. } );
  107. }
  108. /**
  109. * Refreshes the fragments on the current slide so that they
  110. * have the appropriate classes (.visible + .current-fragment).
  111. *
  112. * @param {number} [index] The index of the current fragment
  113. * @param {array} [fragments] Array containing all fragments
  114. * in the current slide
  115. *
  116. * @return {{shown: array, hidden: array}}
  117. */
  118. update( index, fragments ) {
  119. let changedFragments = {
  120. shown: [],
  121. hidden: []
  122. };
  123. let currentSlide = this.Reveal.getCurrentSlide();
  124. if( currentSlide && this.Reveal.getConfig().fragments ) {
  125. fragments = fragments || this.sort( currentSlide.querySelectorAll( '.fragment' ) );
  126. if( fragments.length ) {
  127. let maxIndex = 0;
  128. if( typeof index !== 'number' ) {
  129. let currentFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
  130. if( currentFragment ) {
  131. index = parseInt( currentFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
  132. }
  133. }
  134. toArray( fragments ).forEach( ( el, i ) => {
  135. if( el.hasAttribute( 'data-fragment-index' ) ) {
  136. i = parseInt( el.getAttribute( 'data-fragment-index' ), 10 );
  137. }
  138. maxIndex = Math.max( maxIndex, i );
  139. // Visible fragments
  140. if( i <= index ) {
  141. if( !el.classList.contains( 'visible' ) ) changedFragments.shown.push( el );
  142. el.classList.add( 'visible' );
  143. el.classList.remove( 'current-fragment' );
  144. // Announce the fragments one by one to the Screen Reader
  145. this.Reveal.announceStatus( this.Reveal.getStatusText( el ) );
  146. if( i === index ) {
  147. el.classList.add( 'current-fragment' );
  148. this.Reveal.slideContent.startEmbeddedContent( el );
  149. }
  150. }
  151. // Hidden fragments
  152. else {
  153. if( el.classList.contains( 'visible' ) ) changedFragments.hidden.push( el );
  154. el.classList.remove( 'visible' );
  155. el.classList.remove( 'current-fragment' );
  156. }
  157. } );
  158. // Write the current fragment index to the slide <section>.
  159. // This can be used by end users to apply styles based on
  160. // the current fragment index.
  161. index = typeof index === 'number' ? index : -1;
  162. index = Math.max( Math.min( index, maxIndex ), -1 );
  163. currentSlide.setAttribute( 'data-fragment', index );
  164. }
  165. }
  166. return changedFragments;
  167. }
  168. /**
  169. * Navigate to the specified slide fragment.
  170. *
  171. * @param {?number} index The index of the fragment that
  172. * should be shown, -1 means all are invisible
  173. * @param {number} offset Integer offset to apply to the
  174. * fragment index
  175. *
  176. * @return {boolean} true if a change was made in any
  177. * fragments visibility as part of this call
  178. */
  179. goto( index, offset = 0 ) {
  180. let currentSlide = this.Reveal.getCurrentSlide();
  181. if( currentSlide && this.Reveal.getConfig().fragments ) {
  182. let fragments = this.sort( currentSlide.querySelectorAll( '.fragment' ) );
  183. if( fragments.length ) {
  184. // If no index is specified, find the current
  185. if( typeof index !== 'number' ) {
  186. let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment.visible' ) ).pop();
  187. if( lastVisibleFragment ) {
  188. index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
  189. }
  190. else {
  191. index = -1;
  192. }
  193. }
  194. // Apply the offset if there is one
  195. index += offset;
  196. let changedFragments = this.update( index, fragments );
  197. if( changedFragments.hidden.length ) {
  198. this.Reveal.dispatchEvent( 'fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden } );
  199. }
  200. if( changedFragments.shown.length ) {
  201. this.Reveal.dispatchEvent( 'fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown } );
  202. }
  203. this.Reveal.updateControls();
  204. this.Reveal.updateProgress();
  205. if( this.Reveal.getConfig().fragmentInURL ) {
  206. this.Reveal.writeURL();
  207. }
  208. return !!( changedFragments.shown.length || changedFragments.hidden.length );
  209. }
  210. }
  211. return false;
  212. }
  213. /**
  214. * Navigate to the next slide fragment.
  215. *
  216. * @return {boolean} true if there was a next fragment,
  217. * false otherwise
  218. */
  219. next() {
  220. return this.goto( null, 1 );
  221. }
  222. /**
  223. * Navigate to the previous slide fragment.
  224. *
  225. * @return {boolean} true if there was a previous fragment,
  226. * false otherwise
  227. */
  228. prev() {
  229. return this.goto( null, -1 );
  230. }
  231. }