Controller.ts 8.9 KB

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