import { Direction, DirectionComponent } from '../../components/Direction/Direction'; import { CLASS_ACTIVE, CLASS_CLONE, CLASS_INITIALIZED, CLASS_LIST, CLASS_ROOT, CLASS_SLIDE, CLASS_TRACK, } from '../../constants/classes'; import { DEFAULTS } from '../../constants/defaults'; import { TTB } from '../../constants/directions'; import { EVENT_MOUNTED } from '../../constants/events'; import { LOOP } from '../../constants/types'; import { EventInterface } from '../../constructors'; import { Splide } from '../../core/Splide/Splide'; import { Options } from '../../types'; import { assert, camelToKebab, child, forOwn, isObject, max, merge, push, queryAll, remove, uniqueId, unit, } from '../../utils'; import { Style } from '../Style/Style'; /** * The class to generate static HTML of the slider for the first view. * * @since 3.0.0 */ export class SplideRenderer { /** * Holds slide contents. */ private contents: string[]; /** * The Direction component. */ private Direction: DirectionComponent; /** * Holds the Style instance. */ private Style: Style; /** * Holds options. */ private readonly options: Options = {}; /** * The slider ID. */ private readonly id: string; /** * An array with slide HTML strings. */ private slides: string[]; /** * An array with options for each breakpoint. */ private breakpoints: [ string, Options ][] = []; /** * The SplideRenderer constructor. * * @param contents - An array with slide contents. Each item must be an HTML or a plain text. * @param options - Optional. Options. * @param id - Optional. An ID of the slider. * @param defaults - Static default options. */ constructor( contents: string[], options?: Options, id?: string, defaults: Options = {} ) { merge( DEFAULTS, defaults ); merge( merge( this.options, DEFAULTS ), options || {} ); this.id = id || uniqueId( 'splide' ); this.contents = contents; this.Style = new Style( this.id, this.options ); this.Direction = Direction( null, null, this.options ); assert( this.contents.length, 'Provide at least 1 content.' ); this.init(); } /** * Initializes the instance. */ private init(): void { this.parseBreakpoints(); this.generateSlides(); this.registerRootStyles(); this.registerTrackStyles(); this.registerSlideStyles(); this.registerListStyles(); } /** * Generates HTML of slides with inserting provided contents. */ private generateSlides(): void { this.slides = this.contents.map( ( content, index ) => { return `
  • ${ content }
  • `; } ); if ( this.isLoop() ) { this.generateClones(); } } /** * Generates clones. */ private generateClones(): void { const { classes } = this.options; const count = this.getCloneCount(); const contents = this.contents.slice(); while ( contents.length < count ) { push( contents, contents ); } push( contents.slice( -count ).reverse(), contents.slice( 0, count ) ).forEach( ( content, index ) => { const html = `
  • ${ content }
  • `; index < count ? this.slides.unshift( html ) : this.slides.push( html ); } ); } /** * Returns the number of clones to generate. * * @return A number of clones. */ private getCloneCount(): number { if ( this.isLoop() ) { const { options } = this; if ( options.clones ) { return options.clones; } const perPage = max( ...this.breakpoints.map( ( [ , options ] ) => options.perPage ) ); return perPage * ( ( options.flickMaxPages || 1 ) + 1 ); } return 0; } /** * Registers styles for the root element. */ private registerRootStyles(): void { this.breakpoints.forEach( ( [ width, options ] ) => { this.Style.rule( ' ', 'max-width', unit( options.width ), width ); } ); } /** * Registers styles for the track element. */ private registerTrackStyles(): void { const { Style } = this; const selector = `.${ CLASS_TRACK }`; this.breakpoints.forEach( ( [ width, options ] ) => { Style.rule( selector, this.resolve( 'paddingLeft' ), this.cssPadding( options, false ), width ); Style.rule( selector, this.resolve( 'paddingRight' ), this.cssPadding( options, true ), width ); Style.rule( selector, 'height', this.cssTrackHeight( options ), width ); } ); } /** * Registers styles for the list element. */ private registerListStyles(): void { const { Style, Direction } = this; const selector = `.${ CLASS_LIST }`; this.breakpoints.forEach( ( [ width, options ] ) => { const percent = this.calcOffsetPercent( options ); Style.rule( selector, 'transform', `translate${ Direction.resolve( 'X' ) }(${ percent }%)`, width ); Style.rule( selector, this.resolve( 'left' ), this.cssOffsetLeft( options ), width ); } ); } /** * Registers styles for slides and clones. */ private registerSlideStyles(): void { const { Style } = this; const selector = `.${ CLASS_SLIDE }`; this.breakpoints.forEach( ( [ width, options ] ) => { Style.rule( selector, 'width', this.cssSlideWidth( options ), width ); Style.rule( selector, 'height', this.cssSlideHeight( options ), width ); Style.rule( selector, this.resolve( 'marginRight' ), unit( options.gap ) || '0px', width ); } ); } /** * Returns percentage of the offset for the list element. * This does not include gaps because it can not be converted into percent. * * @return The offset as percent. */ private calcOffsetPercent( options: Options ): number { const slidePercent = 100 / options.perPage; let percent = slidePercent * this.getCloneCount(); if ( options.focus === 'center' ) { if ( this.isLoop() || ! this.options.trimSpace ) { percent -= 50 - slidePercent / 2; } } return this.Direction.orient( percent ); } /** * Returns the value of the left offset for the list element. * * @return The offset as `calc()`. */ private cssOffsetLeft( options: Options ): string { if ( this.isLoop() && options.gap ) { const { perPage } = options; const cssGap = unit( options.gap ) || '0px'; const baseOffset = `-${ cssGap } * ${ this.getCloneCount() / perPage }`; if ( options.focus === 'center' && perPage > 1 ) { return `calc( ${ baseOffset } + ${ cssGap } / 4)`; } else { return `calc(${ baseOffset })`; } } return ''; } /** * Resolves the prop for the current direction and converts it into the Kebab case. * * @param prop - A property name to resolve. * * @return A resolved property name in the Kebab case. */ private resolve( prop: string ): string { return camelToKebab( this.Direction.resolve( prop ) ); } /** * Returns padding in the CSS format. * * @param options - Options. * @param right - Determines whether to get padding right or left. * * @return Padding in the CSS format. */ private cssPadding( options: Options, right: boolean ): string { const { padding } = options; const prop = this.Direction.resolve( right ? 'right' : 'left', true ); return padding ? unit( padding[ prop ] || ( isObject( padding ) ? '0' : padding ) ) : '0'; } /** * Returns height of the track element in the CSS format. * * @param options - Options. * * @return Height in the CSS format. */ private cssTrackHeight( options: Options ): string { let height = ''; if ( this.isVertical() ) { height = this.cssHeight( options ); assert( height, '"height" is missing.' ); const paddingTop = this.cssPadding( options, false ); const paddingBottom = this.cssPadding( options, true ); if ( paddingTop || paddingBottom ) { height = `calc(${ height }`; height += `${ paddingTop ? ` - ${ paddingTop }` : '' }${ paddingBottom ? ` - ${ paddingBottom }` : '' })`; } } return height; } /** * Returns height provided though options in the CSS format. * * @param options - Options. * * @return Height in the CSS format. */ private cssHeight( options: Options ): string { return unit( options.height ); } /** * Returns width of each slide in the CSS format. * * @param options - Options. * * @return Width in the CSS format. */ private cssSlideWidth( options: Options ): string { return options.autoWidth ? '' : unit( options.fixedWidth ) || ( this.isVertical() ? '' : this.cssSlideSize( options ) ); } /** * Returns height of each slide in the CSS format. * * @param options - Options. * * @return Height in the CSS format. */ private cssSlideHeight( options: Options ): string { return unit( options.fixedHeight ) || ( this.isVertical() ? ( options.autoHeight ? '' : this.cssSlideSize( options ) ) : this.cssHeight( options ) ); } /** * Returns width or height of each slide in the CSS format, considering the current direction. * * @param options - Options. * * @return Width or height in the CSS format. */ private cssSlideSize( options: Options ): string { const gap = unit( options.gap ); return `calc((100%${ gap && ` + ${ gap }` })/${ options.perPage || 1 }${ gap && ` - ${ gap }` })`; } /** * Parses breakpoints and generate options for each breakpoint. */ private parseBreakpoints(): void { const { breakpoints } = this.options; this.breakpoints.push( [ 'default', this.options ] ); if ( breakpoints ) { forOwn( breakpoints, ( options, width ) => { this.breakpoints.push( [ width, merge( merge( {}, this.options ), options ) ] ); } ); } } /** * Checks if the slider type is loop or not. * * @return `true` if the slider type is loop, or otherwise `false`. */ private isLoop(): boolean { return this.options.type === LOOP; } /** * Checks if the direction is TTB or not. * * @return `true` if the direction is TTB, or otherwise `false`. */ private isVertical(): boolean { return this.options.direction === TTB; } /** * Builds classes of the root element. * * @return Classes for the root element as a single string. */ private buildClasses(): string { const { options } = this; return [ CLASS_ROOT, `${ CLASS_ROOT }--${ options.type }`, `${ CLASS_ROOT }--${ options.direction }`, CLASS_ACTIVE, CLASS_INITIALIZED, // todo ].filter( Boolean ).join( ' ' ); } /** * Returns the HTML of the slider. * * @return The generated HTML. */ html(): string { let html = ''; html += `
    `; html += ``; html += `
    `; html += ``; html += `
    `; html += `
    `; return html; } /** * Removes a style element and clones. * * @param splide - A Splide instance. */ clean( splide: Splide ): void { const { on } = EventInterface( splide ); const { root } = splide; const clones = queryAll( root, `.${ CLASS_CLONE }` ); on( EVENT_MOUNTED, () => { remove( child( root, 'style' ) ); } ); remove( clones ); } }