Controller.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { EVENT_REFRESH, EVENT_SCROLLED, EVENT_UPDATED } from '../../constants/events';
  2. import { DEFAULT_EVENT_PRIORITY } from '../../constants/priority';
  3. import { LOOP, SLIDE } from '../../constants/types';
  4. import { EventInterface } from '../../constructors';
  5. import { Splide } from '../../core/Splide/Splide';
  6. import { AnyFunction, BaseComponent, Components, Options } from '../../types';
  7. import { approximatelyEqual, between, clamp, floor, isString, isUndefined, max } from '../../utils';
  8. /**
  9. * The interface for the Controller component.
  10. *
  11. * @since 3.0.0
  12. */
  13. export interface ControllerComponent extends BaseComponent {
  14. go( control: number | string, allowSameIndex?: boolean, callback?: AnyFunction ): void;
  15. getNext( destination?: boolean ): number;
  16. getPrev( destination?: boolean ): number;
  17. getEnd(): number;
  18. setIndex( index: number ): void;
  19. getIndex( prev?: boolean ): number;
  20. toIndex( page: number ): number;
  21. toPage( index: number ): number;
  22. hasFocus(): boolean;
  23. }
  24. /**
  25. * The component for controlling the slider.
  26. *
  27. * @since 3.0.0
  28. *
  29. * @param Splide - A Splide instance.
  30. * @param Components - A collection of components.
  31. * @param options - Options.
  32. *
  33. * @return A Controller component object.
  34. */
  35. export function Controller( Splide: Splide, Components: Components, options: Options ): ControllerComponent {
  36. const { on } = EventInterface( Splide );
  37. const { Move } = Components;
  38. const { getPosition, getLimit } = Move;
  39. const { isEnough, getLength } = Components.Slides;
  40. const isLoop = Splide.is( LOOP );
  41. /**
  42. * The current index.
  43. */
  44. let currIndex = options.start || 0;
  45. /**
  46. * The previous index.
  47. */
  48. let prevIndex = currIndex;
  49. /**
  50. * The latest number of slides.
  51. */
  52. let slideCount: number;
  53. /**
  54. * The latest `perMove` value.
  55. */
  56. let perMove: number;
  57. /**
  58. * The latest `perMove` value.
  59. */
  60. let perPage: number;
  61. /**
  62. * Called when the component is mounted.
  63. */
  64. function mount(): void {
  65. init();
  66. on( [ EVENT_UPDATED, EVENT_REFRESH ], init, DEFAULT_EVENT_PRIORITY - 1 );
  67. on( EVENT_SCROLLED, reindex, 0 );
  68. }
  69. /**
  70. * Initializes some parameters.
  71. * Needs to check the slides length since the current index may be out of the range after refresh.
  72. */
  73. function init(): void {
  74. slideCount = getLength( true );
  75. perMove = options.perMove;
  76. perPage = options.perPage;
  77. currIndex = clamp( currIndex, 0, slideCount - 1 );
  78. }
  79. /**
  80. * Calculates the index by the current position and updates the current index.
  81. */
  82. function reindex(): void {
  83. setIndex( Move.toIndex( getPosition() ) );
  84. }
  85. /**
  86. * Moves the slider by the control pattern.
  87. *
  88. * @see `Splide#go()`
  89. *
  90. * @param control - A control pattern.
  91. * @param allowSameIndex - Optional. Determines whether to allow to go to the current index or not.
  92. * @param callback - Optional. A callback function invoked after transition ends.
  93. */
  94. function go( control: number | string, allowSameIndex?: boolean, callback?: AnyFunction ): void {
  95. const dest = parse( control );
  96. const index = loop( dest );
  97. if ( index > -1 && ! Move.isBusy() && ( allowSameIndex || index !== currIndex ) ) {
  98. setIndex( index );
  99. Move.move( dest, index, prevIndex, callback );
  100. }
  101. }
  102. /**
  103. * Parses the control and returns a slide index.
  104. *
  105. * @param control - A control pattern to parse.
  106. */
  107. function parse( control: number | string ): number {
  108. let index = currIndex;
  109. if ( isString( control ) ) {
  110. const [ , indicator, number ] = control.match( /([+\-<>])(\d+)?/ ) || [];
  111. if ( indicator === '+' || indicator === '-' ) {
  112. index = computeDestIndex( currIndex + +`${ indicator }${ +number || 1 }`, currIndex, true );
  113. } else if ( indicator === '>' ) {
  114. index = number ? toIndex( +number ) : getNext( true );
  115. } else if ( indicator === '<' ) {
  116. index = getPrev( true );
  117. }
  118. } else {
  119. if ( isLoop ) {
  120. index = clamp( control, -perPage, slideCount + perPage - 1 );
  121. } else {
  122. index = clamp( control, 0, getEnd() );
  123. }
  124. }
  125. return index;
  126. }
  127. /**
  128. * Returns a next destination index.
  129. *
  130. * @param destination - Optional. Determines whether to get a destination index or a slide one.
  131. *
  132. * @return A next index if available, or otherwise `-1`.
  133. */
  134. function getNext( destination?: boolean ): number {
  135. return getAdjacent( false, destination );
  136. }
  137. /**
  138. * Returns a previous destination index.
  139. *
  140. * @param destination - Optional. Determines whether to get a destination index or a slide one.
  141. *
  142. * @return A previous index if available, or otherwise `-1`.
  143. */
  144. function getPrev( destination?: boolean ): number {
  145. return getAdjacent( true, destination );
  146. }
  147. /**
  148. * Returns an adjacent destination index.
  149. *
  150. * @param prev - Determines whether to return a previous or next index.
  151. * @param destination - Optional. Determines whether to get a destination index or a slide one.
  152. *
  153. * @return An adjacent index if available, or otherwise `-1`.
  154. */
  155. function getAdjacent( prev: boolean, destination?: boolean ): number {
  156. const number = perMove || hasFocus() ? 1 : perPage;
  157. const dest = computeDestIndex( currIndex + number * ( prev ? -1 : 1 ), currIndex );
  158. if ( dest === -1 && Splide.is( SLIDE ) ) {
  159. if ( ! approximatelyEqual( getPosition(), getLimit( ! prev ), 1 ) ) {
  160. return prev ? 0 : getEnd();
  161. }
  162. }
  163. return destination ? dest : loop( dest );
  164. }
  165. /**
  166. * Converts the desired destination index to the valid one.
  167. * - This may return clone indices if the editor is the loop mode,
  168. * or `-1` if there is no slide to go.
  169. * - There are still slides where the slider can go if borders are between `from` and `dest`.
  170. *
  171. * @param dest - The desired destination.
  172. * @param from - A base index.
  173. * @param incremental - Optional. Whether the control is incremental or not.
  174. *
  175. * @return A converted destination index, including clones.
  176. */
  177. function computeDestIndex( dest: number, from: number, incremental?: boolean ): number {
  178. if ( isEnough() ) {
  179. const end = getEnd();
  180. // Will overrun:
  181. if ( dest < 0 || dest > end ) {
  182. if ( between( 0, dest, from, true ) || between( end, from, dest, true ) ) {
  183. dest = toIndex( toPage( dest ) );
  184. } else {
  185. if ( isLoop ) {
  186. dest = perMove
  187. ? dest
  188. : dest < 0 ? - ( slideCount % perPage || perPage ) : slideCount;
  189. } else if ( options.rewind ) {
  190. dest = dest < 0 ? end : 0;
  191. } else {
  192. dest = -1;
  193. }
  194. }
  195. } else {
  196. if ( ! isLoop && ! incremental && dest !== from ) {
  197. dest = perMove ? dest : toIndex( toPage( from ) + ( dest < from ? -1 : 1 ) );
  198. }
  199. }
  200. } else {
  201. dest = -1;
  202. }
  203. return dest;
  204. }
  205. /**
  206. * Returns the end index where the slider can go.
  207. * For example, if the slider has 10 slides and the `perPage` option is 3,
  208. * the slider can go to the slide 8 (the index is 7).
  209. *
  210. * @return An end index.
  211. */
  212. function getEnd(): number {
  213. let end = slideCount - perPage;
  214. if ( hasFocus() || ( isLoop && perMove ) ) {
  215. end = slideCount - 1;
  216. }
  217. return max( end, 0 );
  218. }
  219. /**
  220. * Loops the provided index only in the loop mode.
  221. *
  222. * @param index - An index to loop.
  223. *
  224. * @return A looped index.
  225. */
  226. function loop( index: number ): number {
  227. if ( isLoop ) {
  228. return isEnough() ? index % slideCount + ( index < 0 ? slideCount : 0 ) : -1;
  229. }
  230. return index;
  231. }
  232. /**
  233. * Converts the page index to the slide index.
  234. *
  235. * @param page - A page index to convert.
  236. *
  237. * @return A slide index.
  238. */
  239. function toIndex( page: number ): number {
  240. return clamp( hasFocus() ? page : perPage * page, 0, getEnd() );
  241. }
  242. /**
  243. * Converts the slide index to the page index.
  244. *
  245. * @param index - An index to convert.
  246. */
  247. function toPage( index: number ): number {
  248. if ( ! hasFocus() ) {
  249. index = between( index, slideCount - perPage, slideCount - 1 ) ? slideCount - 1 : index;
  250. index = floor( index / perPage );
  251. }
  252. return index;
  253. }
  254. /**
  255. * Sets a new index and retains old one.
  256. *
  257. * @param index - A new index to set.
  258. */
  259. function setIndex( index: number ): void {
  260. if ( index !== currIndex ) {
  261. prevIndex = currIndex;
  262. currIndex = index;
  263. }
  264. }
  265. /**
  266. * Returns the current/previous index.
  267. *
  268. * @param prev - Optional. Whether to return previous index or not.
  269. */
  270. function getIndex( prev?: boolean ): number {
  271. return prev ? prevIndex : currIndex;
  272. }
  273. /**
  274. * Verifies if the focus option is available or not.
  275. *
  276. * @return `true` if the slider has the focus option.
  277. */
  278. function hasFocus(): boolean {
  279. return ! isUndefined( options.focus ) || options.isNavigation;
  280. }
  281. return {
  282. mount,
  283. go,
  284. getNext,
  285. getPrev,
  286. getEnd,
  287. setIndex,
  288. getIndex,
  289. toIndex,
  290. toPage,
  291. hasFocus,
  292. };
  293. }