Slide.ts 7.8 KB

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