fragments.js 8.9 KB

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