Slide.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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, EVENT_NAVIGATION_MOUNTED,
  25. EVENT_REFRESH,
  26. EVENT_REPOSITIONED,
  27. EVENT_SCROLLED,
  28. EVENT_SLIDE_KEYDOWN,
  29. EVENT_VISIBLE,
  30. } from '../../constants/events';
  31. import { FADE, LOOP } from '../../constants/types';
  32. import { EventInterface } from '../../constructors';
  33. import { Splide } from '../../core/Splide/Splide';
  34. import { BaseComponent } from '../../types';
  35. import {
  36. abs,
  37. ceil,
  38. child,
  39. floor,
  40. format,
  41. getAttribute,
  42. hasClass,
  43. min,
  44. pad,
  45. queryAll,
  46. rect,
  47. removeAttribute,
  48. removeClass,
  49. setAttribute,
  50. style as _style,
  51. toggleClass,
  52. } from '../../utils';
  53. /**
  54. * The interface for the Slide sub component.
  55. *
  56. * @since 3.0.0
  57. */
  58. export interface SlideComponent extends BaseComponent {
  59. index: number;
  60. slideIndex: number;
  61. slide: HTMLElement;
  62. container: HTMLElement;
  63. isClone: boolean;
  64. style( prop: string, value: string | number, useContainer?: boolean ): void
  65. isWithin( from: number, distance: number ): boolean;
  66. }
  67. /**
  68. * The sub component for managing each slide.
  69. *
  70. * @since 3.0.0
  71. *
  72. * @param Splide - A Splide instance.
  73. * @param index - A slide index.
  74. * @param slideIndex - A slide index for clones. This must be `-1` if the slide is not a clone.
  75. * @param slide - A slide element.
  76. *
  77. * @return A Slide sub component.
  78. */
  79. export function Slide( Splide: Splide, index: number, slideIndex: number, slide: HTMLElement ): SlideComponent {
  80. const { on, emit, bind, destroy: destroyEvents } = EventInterface( Splide );
  81. const { Components, root, options } = Splide;
  82. const { isNavigation, updateOnMove } = options;
  83. const { resolve } = Components.Direction;
  84. const styles = getAttribute( slide, 'style' );
  85. const isClone = slideIndex > -1;
  86. const container = child( slide, `.${ CLASS_CONTAINER }` );
  87. const focusableNodes = options.focusableNodes && queryAll( slide, options.focusableNodes );
  88. /**
  89. * Turns into `true` when the component is destroyed.
  90. */
  91. let destroyed: boolean;
  92. /**
  93. * Called when the component is mounted.
  94. */
  95. function mount( this: SlideComponent ): void {
  96. if ( ! isClone ) {
  97. slide.id = `${ root.id }-slide${ pad( index + 1 ) }`;
  98. }
  99. bind( slide, 'click keydown', e => {
  100. emit( e.type === 'click' ? EVENT_CLICK : EVENT_SLIDE_KEYDOWN, this, e );
  101. } );
  102. on( [ EVENT_REFRESH, EVENT_REPOSITIONED, EVENT_MOVED, EVENT_SCROLLED ], update.bind( this ) );
  103. on( EVENT_NAVIGATION_MOUNTED, initNavigation.bind( this ) );
  104. if ( updateOnMove ) {
  105. on( EVENT_MOVE, onMove.bind( this ) );
  106. }
  107. }
  108. /**
  109. * Destroys the component.
  110. */
  111. function destroy(): void {
  112. destroyed = true;
  113. destroyEvents();
  114. removeClass( slide, STATUS_CLASSES );
  115. removeAttribute( slide, ALL_ATTRIBUTES );
  116. setAttribute( slide, 'style', styles );
  117. }
  118. /**
  119. * Initializes slides as navigation.
  120. */
  121. function initNavigation( this: SlideComponent ): void {
  122. const idx = isClone ? slideIndex : index;
  123. const label = format( options.i18n.slideX, idx + 1 );
  124. const controls = Splide.splides.map( target => target.splide.root.id ).join( ' ' );
  125. setAttribute( slide, ARIA_LABEL, label );
  126. setAttribute( slide, ARIA_CONTROLS, controls );
  127. setAttribute( slide, ROLE, 'menuitem' );
  128. updateActivity.call( this, isActive() );
  129. }
  130. /**
  131. * If the `updateOnMove` option is `true`, called when the slider starts moving.
  132. *
  133. * @param next - A next index.
  134. * @param prev - A previous index.
  135. * @param dest - A destination index.
  136. */
  137. function onMove( this: SlideComponent, next: number, prev: number, dest: number ): void {
  138. if ( ! destroyed ) {
  139. update.call( this );
  140. if ( dest === index ) {
  141. updateActivity.call( this, true );
  142. }
  143. }
  144. }
  145. /**
  146. * Updates attribute and classes of the slide.
  147. */
  148. function update( this: SlideComponent ): void {
  149. if ( ! destroyed ) {
  150. const { index: currIndex } = Splide;
  151. updateActivity.call( this, isActive() );
  152. updateVisibility.call( this, isVisible() );
  153. toggleClass( slide, CLASS_PREV, index === currIndex - 1 );
  154. toggleClass( slide, CLASS_NEXT, index === currIndex + 1 );
  155. }
  156. }
  157. /**
  158. * Updates the status related with activity.
  159. *
  160. * @param active - Set `true` if the slide is active.
  161. */
  162. function updateActivity( this: SlideComponent, active: boolean ): void {
  163. if ( active !== hasClass( slide, CLASS_ACTIVE ) ) {
  164. toggleClass( slide, CLASS_ACTIVE, active );
  165. if ( isNavigation ) {
  166. setAttribute( slide, ARIA_CURRENT, active || null );
  167. }
  168. emit( active ? EVENT_ACTIVE : EVENT_INACTIVE, this );
  169. }
  170. }
  171. /**
  172. * Updates classes and attributes related with visibility.
  173. *
  174. * @param visible - Set `true` if the slide is visible.
  175. */
  176. function updateVisibility( this: SlideComponent, visible: boolean ): void {
  177. const ariaHidden = ! visible && ! isActive();
  178. setAttribute( slide, ARIA_HIDDEN, ariaHidden || null );
  179. setAttribute( slide, TAB_INDEX, ! ariaHidden && options.slideFocus ? 0 : null );
  180. if ( focusableNodes ) {
  181. focusableNodes.forEach( node => {
  182. setAttribute( node, TAB_INDEX, ariaHidden ? -1 : null );
  183. } );
  184. }
  185. if ( visible !== hasClass( slide, CLASS_VISIBLE ) ) {
  186. toggleClass( slide, CLASS_VISIBLE, visible );
  187. emit( visible ? EVENT_VISIBLE : EVENT_HIDDEN, this );
  188. }
  189. }
  190. /**
  191. * Adds a CSS rule to the slider or the container.
  192. *
  193. * @param prop - A property name.
  194. * @param value - A CSS value to add.
  195. * @param useContainer - Optional. Determines whether to apply the rule to the container or not.
  196. */
  197. function style( prop: string, value: string | number, useContainer?: boolean ): void {
  198. _style( ( useContainer && container ) || slide, 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 ( ! isClone && ( options.rewind || Splide.is( LOOP ) ) ) {
  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. style,
  247. isWithin,
  248. };
  249. }