Slide.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import {
  2. ALL_ATTRIBUTES,
  3. ARIA_CONTROLS,
  4. ARIA_CURRENT,
  5. ARIA_HIDDEN,
  6. ARIA_LABEL,
  7. ROLE,
  8. TAB_INDEX,
  9. } from '../../constants/attributes';
  10. import {
  11. CLASS_ACTIVE,
  12. CLASS_CONTAINER,
  13. CLASS_NEXT,
  14. CLASS_PREV,
  15. CLASS_VISIBLE,
  16. STATUS_CLASSES,
  17. } from '../../constants/classes';
  18. import {
  19. EVENT_ACTIVE,
  20. EVENT_CLICK,
  21. EVENT_HIDDEN,
  22. EVENT_INACTIVE,
  23. EVENT_MOVE,
  24. EVENT_MOVED,
  25. EVENT_REFRESH,
  26. EVENT_RESIZED,
  27. EVENT_SCROLLED,
  28. EVENT_SLIDE_KEYDOWN,
  29. EVENT_UPDATED,
  30. EVENT_VISIBLE,
  31. } from '../../constants/events';
  32. import { FADE, SLIDE } from '../../constants/types';
  33. import { EventInterface } from '../../constructors';
  34. import { Splide } from '../../core/Splide/Splide';
  35. import { BaseComponent } from '../../types';
  36. import {
  37. abs,
  38. ceil,
  39. child,
  40. floor,
  41. format,
  42. hasClass,
  43. min,
  44. pad,
  45. queryAll,
  46. rect,
  47. removeAttribute,
  48. removeClass,
  49. setAttribute,
  50. toggleClass,
  51. } from '../../utils';
  52. /**
  53. * The interface for the Slide sub component.
  54. *
  55. * @since 3.0.0
  56. */
  57. export interface SlideComponent extends BaseComponent {
  58. index: number;
  59. slideIndex: number;
  60. slide: HTMLElement;
  61. container: HTMLElement;
  62. isClone: boolean;
  63. rule( prop: string, value: string | number, useContainer?: boolean ): void
  64. isWithin( from: number, distance: number ): boolean;
  65. }
  66. /**
  67. * The sub component for managing each slide.
  68. *
  69. * @since 3.0.0
  70. *
  71. * @param Splide - A Splide instance.
  72. * @param index - A slide index.
  73. * @param slideIndex - A slide index for clones. This must be `-1` if the slide is not clone.
  74. * @param slide - A slide element.
  75. *
  76. * @return A Slide sub component.
  77. */
  78. export function Slide( Splide: Splide, index: number, slideIndex: number, slide: HTMLElement ): SlideComponent {
  79. const { on, emit, bind, destroy: destroyEvents } = EventInterface( Splide );
  80. const { Components, root, options } = Splide;
  81. const { isNavigation, updateOnMove } = options;
  82. const { resolve } = Components.Direction;
  83. const isClone = slideIndex > -1;
  84. const container = child( slide, `.${ CLASS_CONTAINER }` );
  85. /**
  86. * Turns into `true` when the component is destroyed.
  87. */
  88. let destroyed: boolean;
  89. /**
  90. * Called when the component is mounted.
  91. */
  92. function mount( this: SlideComponent ): void {
  93. init();
  94. bind( slide, 'click keydown', e => {
  95. emit( e.type === 'click' ? EVENT_CLICK : EVENT_SLIDE_KEYDOWN, this, e );
  96. } );
  97. on( [ EVENT_RESIZED, EVENT_MOVED, EVENT_UPDATED, EVENT_REFRESH, EVENT_SCROLLED ], update.bind( this ) );
  98. if ( updateOnMove ) {
  99. on( EVENT_MOVE, onMove.bind( this ) );
  100. }
  101. update.call( this );
  102. }
  103. /**
  104. * Initializes the component.
  105. */
  106. function init(): void {
  107. if ( ! isClone ) {
  108. slide.id = `${ root.id }-slide${ pad( index + 1 ) }`;
  109. }
  110. if ( isNavigation ) {
  111. const idx = isClone ? slideIndex : index;
  112. const label = format( options.i18n.slideX, idx + 1 );
  113. const controls = Splide.splides.map( splide => splide.root.id ).join( ' ' );
  114. setAttribute( slide, ARIA_LABEL, label );
  115. setAttribute( slide, ARIA_CONTROLS, controls );
  116. setAttribute( slide, ROLE, 'menuitem' );
  117. }
  118. }
  119. /**
  120. * Destroys the component.
  121. */
  122. function destroy(): void {
  123. destroyed = true;
  124. destroyEvents();
  125. removeClass( slide, STATUS_CLASSES );
  126. removeAttribute( slide, ALL_ATTRIBUTES );
  127. }
  128. /**
  129. * If the `updateOnMove` option is `true`, called when the slider starts moving.
  130. *
  131. * @param next - A next index.
  132. * @param prev - A previous index.
  133. * @param dest - A destination index.
  134. */
  135. function onMove( this: SlideComponent, next: number, prev: number, dest: number ): void {
  136. if ( ! destroyed ) {
  137. update.call( this );
  138. if ( dest === index ) {
  139. updateActivity.call( this, true );
  140. }
  141. }
  142. }
  143. /**
  144. * Updates attribute and classes of the slide.
  145. */
  146. function update( this: SlideComponent ): void {
  147. if ( ! destroyed ) {
  148. const { index: currIndex } = Splide;
  149. updateActivity.call( this, isActive() );
  150. updateVisibility.call( this, isVisible() );
  151. toggleClass( slide, CLASS_PREV, index === currIndex - 1 );
  152. toggleClass( slide, CLASS_NEXT, index === currIndex + 1 );
  153. }
  154. }
  155. /**
  156. * Updates the status related with activity.
  157. *
  158. * @param active - Set `true` if the slide is active.
  159. */
  160. function updateActivity( this: SlideComponent, active: boolean ): void {
  161. if ( active !== hasClass( slide, CLASS_ACTIVE ) ) {
  162. toggleClass( slide, CLASS_ACTIVE, active );
  163. if ( isNavigation ) {
  164. setAttribute( slide, ARIA_CURRENT, active || null );
  165. }
  166. emit( active ? EVENT_ACTIVE : EVENT_INACTIVE, this );
  167. }
  168. }
  169. /**
  170. * Updates classes and attributes related with visibility.
  171. *
  172. * @param visible - Set `true` if the slide is visible.
  173. */
  174. function updateVisibility( this: SlideComponent, visible: boolean ): void {
  175. const { focusableNodes } = options;
  176. const ariaHidden = ! visible && ! isActive();
  177. setAttribute( slide, ARIA_HIDDEN, ariaHidden || null );
  178. setAttribute( slide, TAB_INDEX, ! ariaHidden && options.slideFocus ? 0 : null );
  179. if ( focusableNodes ) {
  180. queryAll( slide, focusableNodes ).forEach( node => {
  181. setAttribute( node, TAB_INDEX, ariaHidden ? -1 : null );
  182. } );
  183. }
  184. if ( visible !== hasClass( slide, CLASS_VISIBLE ) ) {
  185. toggleClass( slide, CLASS_VISIBLE, visible );
  186. emit( visible ? EVENT_VISIBLE : EVENT_HIDDEN, this );
  187. }
  188. }
  189. /**
  190. * Adds a CSS rule to the slider or the container.
  191. *
  192. * @param prop - A property name.
  193. * @param value - A CSS value to add.
  194. * @param useContainer - Optional. Determines whether to apply the rule to the container or not.
  195. */
  196. function rule( prop: string, value: string | number, useContainer?: boolean ): void {
  197. const selector = `#${ slide.id }${ container && useContainer ? ` > .${ CLASS_CONTAINER }` : '' }`;
  198. Components.Style.rule( selector, prop, value );
  199. }
  200. /**
  201. * Checks if the slide is active or not.
  202. *
  203. * @return `true` if the slide is active.
  204. */
  205. function isActive(): boolean {
  206. return Splide.index === index;
  207. }
  208. /**
  209. * Checks if the slide is visible or not.
  210. */
  211. function isVisible(): boolean {
  212. if ( Splide.is( FADE ) ) {
  213. return isActive();
  214. }
  215. const trackRect = rect( Components.Elements.track );
  216. const slideRect = rect( slide );
  217. const left = resolve( 'left' );
  218. const right = resolve( 'right' );
  219. return floor( trackRect[ left ] ) <= ceil( slideRect[ left ] )
  220. && floor( slideRect[ right ] ) <= ceil( trackRect[ right ] );
  221. }
  222. /**
  223. * Calculates how far this slide is from another slide and
  224. * returns `true` if the distance is within the given number.
  225. *
  226. * @param from - An index of a base slide.
  227. * @param distance - `true` if the slide is within this number.
  228. *
  229. * @return `true` if the slide is within the `distance` from the base slide, or otherwise `false`.
  230. */
  231. function isWithin( from: number, distance: number ): boolean {
  232. let diff = abs( from - index );
  233. if ( ! Splide.is( SLIDE ) && ! isClone ) {
  234. diff = min( diff, Splide.length - diff );
  235. }
  236. return diff <= distance;
  237. }
  238. return {
  239. index,
  240. slideIndex,
  241. slide,
  242. container,
  243. isClone,
  244. mount,
  245. destroy,
  246. rule,
  247. isWithin,
  248. };
  249. }