SplideRenderer.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { Direction, DirectionComponent } from '../../components/Direction/Direction';
  2. import {
  3. CLASS_ACTIVE,
  4. CLASS_CLONE,
  5. CLASS_INITIALIZED,
  6. CLASS_LIST,
  7. CLASS_ROOT,
  8. CLASS_SLIDE,
  9. CLASS_TRACK,
  10. } from '../../constants/classes';
  11. import { DEFAULTS } from '../../constants/defaults';
  12. import { TTB } from '../../constants/directions';
  13. import { EVENT_MOUNTED } from '../../constants/events';
  14. import { LOOP } from '../../constants/types';
  15. import { EventInterface } from '../../constructors';
  16. import { Splide } from '../../core/Splide/Splide';
  17. import { Options } from '../../types';
  18. import {
  19. assert,
  20. camelToKebab,
  21. child,
  22. forOwn,
  23. isObject,
  24. max,
  25. merge,
  26. push,
  27. queryAll,
  28. remove,
  29. uniqueId,
  30. unit,
  31. } from '../../utils';
  32. import { Style } from '../Style/Style';
  33. /**
  34. * The class to generate static HTML of the slider for the first view.
  35. *
  36. * @since 3.0.0
  37. */
  38. export class SplideRenderer {
  39. /**
  40. * Holds slide contents.
  41. */
  42. private contents: string[];
  43. /**
  44. * The Direction component.
  45. */
  46. private Direction: DirectionComponent;
  47. /**
  48. * Holds the Style instance.
  49. */
  50. private Style: Style;
  51. /**
  52. * Holds options.
  53. */
  54. private readonly options: Options = {};
  55. /**
  56. * The slider ID.
  57. */
  58. private readonly id: string;
  59. /**
  60. * An array with slide HTML strings.
  61. */
  62. private slides: string[];
  63. /**
  64. * An array with options for each breakpoint.
  65. */
  66. private breakpoints: [ string, Options ][] = [];
  67. /**
  68. * The SplideRenderer constructor.
  69. *
  70. * @param contents - An array with slide contents. Each item must be an HTML or a plain text.
  71. * @param options - Optional. Options.
  72. * @param id - Optional. An ID of the slider.
  73. * @param defaults - Static default options.
  74. */
  75. constructor( contents: string[], options?: Options, id?: string, defaults: Options = {} ) {
  76. merge( DEFAULTS, defaults );
  77. merge( merge( this.options, DEFAULTS ), options || {} );
  78. this.id = id || uniqueId( 'splide' );
  79. this.contents = contents;
  80. this.Style = new Style( this.id, this.options );
  81. this.Direction = Direction( null, null, this.options );
  82. assert( this.contents.length, 'Provide at least 1 content.' );
  83. this.init();
  84. }
  85. /**
  86. * Initializes the instance.
  87. */
  88. private init(): void {
  89. this.parseBreakpoints();
  90. this.generateSlides();
  91. this.registerRootStyles();
  92. this.registerTrackStyles();
  93. this.registerSlideStyles();
  94. this.registerListStyles();
  95. }
  96. /**
  97. * Generates HTML of slides with inserting provided contents.
  98. */
  99. private generateSlides(): void {
  100. this.slides = this.contents.map( ( content, index ) => {
  101. return `<li class="${ this.options.classes.slide } ${ index === 0 ? CLASS_ACTIVE : '' }">${ content }</li>`;
  102. } );
  103. if ( this.isLoop() ) {
  104. this.generateClones();
  105. }
  106. }
  107. /**
  108. * Generates clones.
  109. */
  110. private generateClones(): void {
  111. const { classes } = this.options;
  112. const count = this.getCloneCount();
  113. const contents = this.contents.slice();
  114. while ( contents.length < count ) {
  115. push( contents, contents );
  116. }
  117. push( contents.slice( -count ).reverse(), contents.slice( 0, count ) ).forEach( ( content, index ) => {
  118. const html = `<li class="${ classes.slide } ${ classes.clone }">${ content }</li>`;
  119. index < count ? this.slides.unshift( html ) : this.slides.push( html );
  120. } );
  121. }
  122. /**
  123. * Returns the number of clones to generate.
  124. *
  125. * @return A number of clones.
  126. */
  127. private getCloneCount(): number {
  128. if ( this.isLoop() ) {
  129. const { options } = this;
  130. if ( options.clones ) {
  131. return options.clones;
  132. }
  133. const perPage = max( ...this.breakpoints.map( ( [ , options ] ) => options.perPage ) );
  134. return perPage * ( ( options.flickMaxPages || 1 ) + 1 );
  135. }
  136. return 0;
  137. }
  138. /**
  139. * Registers styles for the root element.
  140. */
  141. private registerRootStyles(): void {
  142. this.breakpoints.forEach( ( [ width, options ] ) => {
  143. this.Style.rule( ' ', 'max-width', unit( options.width ), width );
  144. } );
  145. }
  146. /**
  147. * Registers styles for the track element.
  148. */
  149. private registerTrackStyles(): void {
  150. const { Style } = this;
  151. const selector = `.${ CLASS_TRACK }`;
  152. this.breakpoints.forEach( ( [ width, options ] ) => {
  153. Style.rule( selector, this.resolve( 'paddingLeft' ), this.cssPadding( options, false ), width );
  154. Style.rule( selector, this.resolve( 'paddingRight' ), this.cssPadding( options, true ), width );
  155. Style.rule( selector, 'height', this.cssTrackHeight( options ), width );
  156. } );
  157. }
  158. /**
  159. * Registers styles for the list element.
  160. */
  161. private registerListStyles(): void {
  162. const { Style, Direction } = this;
  163. const selector = `.${ CLASS_LIST }`;
  164. this.breakpoints.forEach( ( [ width, options ] ) => {
  165. const percent = this.calcOffsetPercent( options );
  166. Style.rule( selector, 'transform', `translate${ Direction.resolve( 'X' ) }(${ percent }%)`, width );
  167. Style.rule( selector, this.resolve( 'left' ), this.cssOffsetLeft( options ), width );
  168. } );
  169. }
  170. /**
  171. * Registers styles for slides and clones.
  172. */
  173. private registerSlideStyles(): void {
  174. const { Style } = this;
  175. const selector = `.${ CLASS_SLIDE }`;
  176. this.breakpoints.forEach( ( [ width, options ] ) => {
  177. Style.rule( selector, 'width', this.cssSlideWidth( options ), width );
  178. Style.rule( selector, 'height', this.cssSlideHeight( options ), width );
  179. Style.rule( selector, this.resolve( 'marginRight' ), unit( options.gap ) || '0px', width );
  180. } );
  181. }
  182. /**
  183. * Returns percentage of the offset for the list element.
  184. * This does not include gaps because it can not be converted into percent.
  185. *
  186. * @return The offset as percent.
  187. */
  188. private calcOffsetPercent( options: Options ): number {
  189. const slidePercent = 100 / options.perPage;
  190. let percent = slidePercent * this.getCloneCount();
  191. if ( options.focus === 'center' ) {
  192. if ( this.isLoop() || ! this.options.trimSpace ) {
  193. percent -= 50 - slidePercent / 2;
  194. }
  195. }
  196. return this.Direction.orient( percent );
  197. }
  198. /**
  199. * Returns the value of the left offset for the list element.
  200. *
  201. * @return The offset as `calc()`.
  202. */
  203. private cssOffsetLeft( options: Options ): string {
  204. if ( this.isLoop() && options.gap ) {
  205. const { perPage } = options;
  206. const cssGap = unit( options.gap ) || '0px';
  207. const baseOffset = `-${ cssGap } * ${ this.getCloneCount() / perPage }`;
  208. if ( options.focus === 'center' && perPage > 1 ) {
  209. return `calc( ${ baseOffset } + ${ cssGap } / 4)`;
  210. } else {
  211. return `calc(${ baseOffset })`;
  212. }
  213. }
  214. return '';
  215. }
  216. /**
  217. * Resolves the prop for the current direction and converts it into the Kebab case.
  218. *
  219. * @param prop - A property name to resolve.
  220. *
  221. * @return A resolved property name in the Kebab case.
  222. */
  223. private resolve( prop: string ): string {
  224. return camelToKebab( this.Direction.resolve( prop ) );
  225. }
  226. /**
  227. * Returns padding in the CSS format.
  228. *
  229. * @param options - Options.
  230. * @param right - Determines whether to get padding right or left.
  231. *
  232. * @return Padding in the CSS format.
  233. */
  234. private cssPadding( options: Options, right: boolean ): string {
  235. const { padding } = options;
  236. const prop = this.Direction.resolve( right ? 'right' : 'left', true );
  237. return padding ? unit( padding[ prop ] || ( isObject( padding ) ? '0' : padding ) ) : '0';
  238. }
  239. /**
  240. * Returns height of the track element in the CSS format.
  241. *
  242. * @param options - Options.
  243. *
  244. * @return Height in the CSS format.
  245. */
  246. private cssTrackHeight( options: Options ): string {
  247. let height = '';
  248. if ( this.isVertical() ) {
  249. height = this.cssHeight( options );
  250. assert( height, '"height" is missing.' );
  251. const paddingTop = this.cssPadding( options, false );
  252. const paddingBottom = this.cssPadding( options, true );
  253. if ( paddingTop || paddingBottom ) {
  254. height = `calc(${ height }`;
  255. height += `${ paddingTop ? ` - ${ paddingTop }` : '' }${ paddingBottom ? ` - ${ paddingBottom }` : '' })`;
  256. }
  257. }
  258. return height;
  259. }
  260. /**
  261. * Returns height provided though options in the CSS format.
  262. *
  263. * @param options - Options.
  264. *
  265. * @return Height in the CSS format.
  266. */
  267. private cssHeight( options: Options ): string {
  268. return unit( options.height );
  269. }
  270. /**
  271. * Returns width of each slide in the CSS format.
  272. *
  273. * @param options - Options.
  274. *
  275. * @return Width in the CSS format.
  276. */
  277. private cssSlideWidth( options: Options ): string {
  278. return options.autoWidth
  279. ? ''
  280. : unit( options.fixedWidth ) || ( this.isVertical() ? '' : this.cssSlideSize( options ) );
  281. }
  282. /**
  283. * Returns height of each slide in the CSS format.
  284. *
  285. * @param options - Options.
  286. *
  287. * @return Height in the CSS format.
  288. */
  289. private cssSlideHeight( options: Options ): string {
  290. return unit( options.fixedHeight )
  291. || ( this.isVertical()
  292. ? ( options.autoHeight ? '' : this.cssSlideSize( options ) )
  293. : this.cssHeight( options )
  294. );
  295. }
  296. /**
  297. * Returns width or height of each slide in the CSS format, considering the current direction.
  298. *
  299. * @param options - Options.
  300. *
  301. * @return Width or height in the CSS format.
  302. */
  303. private cssSlideSize( options: Options ): string {
  304. const gap = unit( options.gap );
  305. return `calc((100%${ gap && ` + ${ gap }` })/${ options.perPage || 1 }${ gap && ` - ${ gap }` })`;
  306. }
  307. /**
  308. * Parses breakpoints and generate options for each breakpoint.
  309. */
  310. private parseBreakpoints(): void {
  311. const { breakpoints } = this.options;
  312. this.breakpoints.push( [ 'default', this.options ] );
  313. if ( breakpoints ) {
  314. forOwn( breakpoints, ( options, width ) => {
  315. this.breakpoints.push( [ width, merge( merge( {}, this.options ), options ) ] );
  316. } );
  317. }
  318. }
  319. /**
  320. * Checks if the slider type is loop or not.
  321. *
  322. * @return `true` if the slider type is loop, or otherwise `false`.
  323. */
  324. private isLoop(): boolean {
  325. return this.options.type === LOOP;
  326. }
  327. /**
  328. * Checks if the direction is TTB or not.
  329. *
  330. * @return `true` if the direction is TTB, or otherwise `false`.
  331. */
  332. private isVertical(): boolean {
  333. return this.options.direction === TTB;
  334. }
  335. /**
  336. * Builds classes of the root element.
  337. *
  338. * @return Classes for the root element as a single string.
  339. */
  340. private buildClasses(): string {
  341. const { options } = this;
  342. return [
  343. CLASS_ROOT,
  344. `${ CLASS_ROOT }--${ options.type }`,
  345. `${ CLASS_ROOT }--${ options.direction }`,
  346. CLASS_ACTIVE,
  347. CLASS_INITIALIZED, // todo
  348. ].filter( Boolean ).join( ' ' );
  349. }
  350. /**
  351. * Returns the HTML of the slider.
  352. *
  353. * @return The generated HTML.
  354. */
  355. html(): string {
  356. let html = '';
  357. html += `<div id="${ this.id }" class="${ this.buildClasses() }">`;
  358. html += `<style>${ this.Style.build() }</style>`;
  359. html += `<div class="splide__track">`;
  360. html += `<ul class="splide__list">`;
  361. html += this.slides.join( '' );
  362. html += `</ul>`;
  363. html += `</div>`;
  364. html += `</div>`;
  365. return html;
  366. }
  367. /**
  368. * Removes a style element and clones.
  369. *
  370. * @param splide - A Splide instance.
  371. */
  372. clean( splide: Splide ): void {
  373. const { on } = EventInterface( splide );
  374. const { root } = splide;
  375. const clones = queryAll( root, `.${ CLASS_CLONE }` );
  376. on( EVENT_MOUNTED, () => {
  377. remove( child( root, 'style' ) );
  378. } );
  379. remove( clones );
  380. }
  381. }