You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

375 lines
9.1KB

  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. changedFragments.hidden.push( el );
  187. this.Reveal.dispatchEvent({
  188. target: el,
  189. type: 'hidden',
  190. bubbles: false
  191. });
  192. }
  193. }
  194. } );
  195. // Write the current fragment index to the slide <section>.
  196. // This can be used by end users to apply styles based on
  197. // the current fragment index.
  198. index = typeof index === 'number' ? index : -1;
  199. index = Math.max( Math.min( index, maxIndex ), -1 );
  200. currentSlide.setAttribute( 'data-fragment', index );
  201. }
  202. }
  203. return changedFragments;
  204. }
  205. /**
  206. * Formats the fragments on the given slide so that they have
  207. * valid indices. Call this if fragments are changed in the DOM
  208. * after reveal.js has already initialized.
  209. *
  210. * @param {HTMLElement} slide
  211. * @return {Array} a list of the HTML fragments that were synced
  212. */
  213. sync( slide = this.Reveal.getCurrentSlide() ) {
  214. return this.sort( slide.querySelectorAll( '.fragment' ) );
  215. }
  216. /**
  217. * Navigate to the specified slide fragment.
  218. *
  219. * @param {?number} index The index of the fragment that
  220. * should be shown, -1 means all are invisible
  221. * @param {number} offset Integer offset to apply to the
  222. * fragment index
  223. *
  224. * @return {boolean} true if a change was made in any
  225. * fragments visibility as part of this call
  226. */
  227. goto( index, offset = 0 ) {
  228. let currentSlide = this.Reveal.getCurrentSlide();
  229. if( currentSlide && this.Reveal.getConfig().fragments ) {
  230. let fragments = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled)' ) );
  231. if( fragments.length ) {
  232. // If no index is specified, find the current
  233. if( typeof index !== 'number' ) {
  234. let lastVisibleFragment = this.sort( currentSlide.querySelectorAll( '.fragment:not(.disabled).visible' ) ).pop();
  235. if( lastVisibleFragment ) {
  236. index = parseInt( lastVisibleFragment.getAttribute( 'data-fragment-index' ) || 0, 10 );
  237. }
  238. else {
  239. index = -1;
  240. }
  241. }
  242. // Apply the offset if there is one
  243. index += offset;
  244. let changedFragments = this.update( index, fragments );
  245. if( changedFragments.hidden.length ) {
  246. this.Reveal.dispatchEvent({
  247. type: 'fragmenthidden',
  248. data: {
  249. fragment: changedFragments.hidden[0],
  250. fragments: changedFragments.hidden
  251. }
  252. });
  253. }
  254. if( changedFragments.shown.length ) {
  255. this.Reveal.dispatchEvent({
  256. type: 'fragmentshown',
  257. data: {
  258. fragment: changedFragments.shown[0],
  259. fragments: changedFragments.shown
  260. }
  261. });
  262. }
  263. this.Reveal.controls.update();
  264. this.Reveal.progress.update();
  265. if( this.Reveal.getConfig().fragmentInURL ) {
  266. this.Reveal.location.writeURL();
  267. }
  268. return !!( changedFragments.shown.length || changedFragments.hidden.length );
  269. }
  270. }
  271. return false;
  272. }
  273. /**
  274. * Navigate to the next slide fragment.
  275. *
  276. * @return {boolean} true if there was a next fragment,
  277. * false otherwise
  278. */
  279. next() {
  280. return this.goto( null, 1 );
  281. }
  282. /**
  283. * Navigate to the previous slide fragment.
  284. *
  285. * @return {boolean} true if there was a previous fragment,
  286. * false otherwise
  287. */
  288. prev() {
  289. return this.goto( null, -1 );
  290. }
  291. }