123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707 |
- import { PATH, SIZE, XML_NAME_SPACE } from '../../components/Arrows/path';
- import { Direction, DirectionComponent } from '../../components/Direction/Direction';
- import { CLASS_ACTIVE, CLASS_CLONE, 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, SLIDE } from '../../constants/types';
- import { EventInterface } from '../../constructors';
- import { Splide } from '../../core/Splide/Splide';
- import { Options } from '../../types';
- import {
- assert,
- assign,
- camelToKebab,
- child,
- forOwn,
- isObject,
- isString,
- max,
- merge,
- push,
- queryAll,
- remove,
- uniqueId,
- unit,
- } from '../../utils';
- import { CLASS_RENDERED } from '../constants/classes';
- import { RENDERER_DEFAULT_CONFIG } from '../constants/defaults';
- import { Style } from '../Style/Style';
- import { RendererConfig, SlideContent } from '../types/types';
- /**
- * The class to generate static HTML of the slider for the first view.
- *
- * @since 3.0.0
- */
- export class SplideRenderer {
- /**
- * Removes a style element and clones.
- *
- * @param splide - A Splide instance.
- */
- static 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 );
- }
- /**
- * Holds slide contents.
- */
- private readonly contents: string[] | SlideContent[];
- /**
- * Stores data of slides.
- */
- private readonly slides: SlideContent[] = [];
- /**
- * The Direction component.
- */
- private readonly Direction: DirectionComponent;
- /**
- * Holds the Style instance.
- */
- private readonly Style: Style;
- /**
- * Holds options.
- */
- private readonly options: Options = {};
- /**
- * Holds options for this instance.
- */
- private readonly config: RendererConfig;
- /**
- * The slider ID.
- */
- private readonly id: string;
- /**
- * An array with options for each breakpoint.
- */
- private readonly 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. Slider options.
- * @param config - Static default options.
- * @param defaults - Default options for the slider. Pass `Splide.defaults` if you are using it.
- */
- constructor( contents: string[] | SlideContent[], options?: Options, config?: RendererConfig, defaults?: Options ) {
- merge( DEFAULTS, defaults || {} );
- merge( merge( this.options, DEFAULTS ), options || {} );
- this.contents = contents;
- this.config = assign( {}, RENDERER_DEFAULT_CONFIG, config || {} );
- this.id = this.config.id || uniqueId( 'splide' );
- 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.initSlides();
- this.registerRootStyles();
- this.registerTrackStyles();
- this.registerSlideStyles();
- this.registerListStyles();
- }
- /**
- * Initializes slides.
- */
- private initSlides(): void {
- push( this.slides, this.contents.map( ( content, index ) => {
- content = isString( content ) ? { html: content } : content;
- content.styles = content.styles || {};
- this.cover( content );
- assign( ( content.attrs = content.attrs || {} ), {
- class: `${ this.options.classes.slide } ${ index === 0 ? CLASS_ACTIVE : '' }`.trim(),
- style: this.buildStyles( content.styles ),
- } );
- return content;
- } ) );
- if ( this.isLoop() ) {
- this.generateClones( this.slides );
- }
- }
- /**
- * 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 } = this;
- const selector = `.${ CLASS_LIST }`;
- this.breakpoints.forEach( ( [ width, options ] ) => {
- Style.rule( selector, 'transform', this.buildTranslate( 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, this.resolve( 'marginRight' ), unit( options.gap ) || '0px', width );
- const height = this.cssSlideHeight( options );
- if ( height ) {
- Style.rule( selector, 'height', height, width );
- } else {
- Style.rule( selector, 'padding-top', this.cssSlidePadding( options ), width );
- }
- Style.rule( `${ selector } > img`, 'display', options.cover ? 'none' : 'inline', width );
- } );
- }
- /**
- * Builds multiple `translateX` for the list element.
- *
- * @param options - Options for each breakpoint.
- *
- * @return A string with multiple translate functions.
- */
- private buildTranslate( options: Options ): string {
- const { resolve, orient } = this.Direction;
- const values = [];
- values.push( this.cssOffsetClones( options ) );
- values.push( this.cssOffsetGaps( options ) );
- if ( this.isCenter( options ) ) {
- values.push( this.buildCssValue( orient( -50 ), '%' ) );
- values.push( ...this.cssOffsetCenter( options ) );
- }
- return values.map( value => `translate${ resolve( 'X' ) }(${ value })` ).join( ' ' );
- }
- /**
- * Returns offset for the list element.
- * This does not include gaps because it can not be converted into percent.
- *
- * @param options - Options for each breakpoint.
- *
- * @return The offset.
- */
- private cssOffsetClones( options: Options ): string {
- const { resolve, orient } = this.Direction;
- const cloneCount = this.getCloneCount();
- if ( this.isFixedWidth( options ) ) {
- const { value, unit } = this.parseCssValue( options[ resolve( 'fixedWidth' ) ] );
- return this.buildCssValue( orient( value ) * cloneCount, unit );
- }
- const percent = 100 * cloneCount / options.perPage;
- return `${ orient( percent ) }%`;
- }
- /**
- * Returns offset for centering the active slide.
- *
- * Note:
- * ( 100% + gap ) / perPage - gap
- * 100% / perPage + gap / perPage - gap;
- * 50% / perPage + ( gap / perPage - gap ) / 2;
- *
- * @param options - Options for each breakpoint.
- *
- * @return The offset.
- */
- private cssOffsetCenter( options: Options ): string[] {
- const { resolve, orient } = this.Direction;
- if ( this.isFixedWidth( options ) ) {
- const { value, unit } = this.parseCssValue( options[ resolve( 'fixedWidth' ) ] );
- return [ this.buildCssValue( orient( value / 2 ), unit ) ];
- }
- const values = [];
- const { perPage, gap } = options;
- values.push( `${ orient( 50 / perPage ) }%` );
- if ( gap ) {
- const { value, unit } = this.parseCssValue( gap );
- const gapOffset = ( value / perPage - value ) / 2;
- values.push( this.buildCssValue( orient( gapOffset ), unit ) );
- }
- return values;
- }
- /**
- * Returns offset for gaps.
- *
- * @param options - Options for each breakpoint.
- *
- * @return The offset as `calc()`.
- */
- private cssOffsetGaps( options: Options ): string {
- const cloneCount = this.getCloneCount();
- if ( cloneCount && options.gap ) {
- const { orient } = this.Direction;
- const { value, unit } = this.parseCssValue( options.gap );
- if ( this.isFixedWidth( options ) ) {
- return this.buildCssValue( orient( value * cloneCount ), unit );
- }
- const { perPage } = options;
- const gaps = cloneCount / perPage;
- return this.buildCssValue( orient( gaps * value ), unit );
- }
- 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 ) ) || '0px';
- }
- /**
- * 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.' );
- height = `calc(${ height } - ${ this.cssPadding( options, false ) } - ${ this.cssPadding( options, true ) })`;
- }
- 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 }` })`;
- }
- /**
- * Returns the paddingTop value to simulate the height of each slide.
- *
- * @param options - Options.
- *
- * @return paddingTop in the CSS format.
- */
- private cssSlidePadding( options: Options ): string {
- const { heightRatio } = options;
- return heightRatio ? `${ heightRatio * 100 }%` : '';
- }
- /**
- * Builds the css value by the provided value and unit.
- *
- * @param value - A value.
- * @param unit - A CSS unit.
- *
- * @return A built value for a CSS value.
- */
- private buildCssValue( value: number, unit: string ): string {
- return `${ value }${ unit }`;
- }
- /**
- * Parses the CSS value into number and unit.
- *
- * @param value - A value to parse.
- *
- * @return An object with value and unit.
- */
- private parseCssValue( value: string | number ): { value: number, unit: string } {
- if ( isString( value ) ) {
- const number = parseFloat( value ) || 0;
- const unit = value.replace( /\d*(\.\d*)?/, '' ) || 'px';
- return { value: number, unit };
- }
- return { value, unit: 'px' };
- }
- /**
- * 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 slide width is fixed or not.
- *
- * @return `true` if the slide width is fixed, or otherwise `false`.
- */
- private isFixedWidth( options: Options ): boolean {
- return !! options[ this.Direction.resolve( 'fixedWidth' ) ];
- }
- /**
- * Checks if the `autoWidth` is active or not.
- *
- * @return `true` if the `autoWidth` is active, or otherwise `false`.
- */
- private isAutoWidth( options: Options ): boolean {
- return !! options[ this.Direction.resolve( 'autoWidth' ) ];
- }
- /**
- * 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 active slide should be centered or not.
- *
- * @return `true` if the slide should be centered, or otherwise `false`.
- */
- private isCenter( options: Options ): boolean {
- if( options.focus === 'center' ) {
- if ( this.isLoop() ) {
- return true;
- }
- if ( this.options.type === SLIDE ) {
- return ! this.options.trimSpace;
- }
- }
- return false;
- }
- /**
- * 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,
- ! this.config.hidden && CLASS_RENDERED,
- ].filter( Boolean ).join( ' ' );
- }
- /**
- * Converts provided attributes into a single string.
- *
- * @param attrs - An object with attributes.
- *
- * @return A built string.
- */
- private buildAttrs( attrs: Record<string, string | number | boolean> ): string {
- let attr = '';
- forOwn( attrs, ( value, key ) => {
- attr += value ? ` ${ camelToKebab( key ) }="${ value }"` : '';
- } );
- return attr.trim();
- }
- /**
- * Converts provided styles into a single string.
- *
- * @param styles - An object with styles.
- *
- * @return A built string.
- */
- private buildStyles( styles: Record<string, string | number> ): string {
- let style = '';
- forOwn( styles, ( value, key ) => {
- style += ` ${ camelToKebab( key ) }:${ value };`;
- } );
- return style.trim();
- }
- /**
- * Generates HTML of slides with inserting provided contents.
- *
- * @return The HTML for all slides and clones.
- */
- private renderSlides(): string {
- const { slideTag: tag } = this.config;
- return this.slides.map( content => {
- return `<${ tag } ${ this.buildAttrs( content.attrs ) }>${ content.html || '' }</${ tag }>`;
- } ).join( '' );
- }
- /**
- * Add the `background` style for the cover mode.
- *
- * @param content - A slide content.
- */
- private cover( content: SlideContent ): void {
- const { styles, html = '' } = content;
- if ( this.options.cover ) {
- const src = html.match( /<img.*?src\s*=\s*(['"])(.+?)\1.*?>/ );
- if ( src && src[ 2 ] ) {
- styles.background = `center/cover no-repeat url('${ src[ 2 ] }')`;
- }
- }
- }
- /**
- * Generates clones.
- *
- * @param contents - An array with SlideContent objects.
- */
- private generateClones( contents: SlideContent[] ): void {
- const { classes } = this.options;
- const count = this.getCloneCount();
- const slides = contents.slice();
- while ( slides.length < count ) {
- push( slides, slides );
- }
- push( slides.slice( -count ).reverse(), slides.slice( 0, count ) ).forEach( ( content, index ) => {
- const attrs = assign( {}, content.attrs, { class: `${ content.attrs.class } ${ classes.clone }` } );
- const clone = assign( {}, content, { attrs } );
- index < count ? contents.unshift( clone ) : contents.push( clone );
- } );
- }
- /**
- * 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;
- }
- /**
- * Generates arrows and the wrapper element.
- *
- * @return The HTML for arrows.
- */
- private renderArrows(): string {
- let html = '';
- html += `<div class="${ this.options.classes.arrows }">`;
- html += this.renderArrow( true );
- html += this.renderArrow( false );
- html += `</div>`;
- return html;
- }
- /**
- * Generates an arrow HTML.
- *
- * @param prev - Options for each breakpoint.
- *
- * @return The HTML for the prev or next arrow.
- */
- private renderArrow( prev: boolean ): string {
- const { classes } = this.options;
- return `<button class="${ classes.arrow } ${ prev ? classes.prev : classes.next }" type="button">`
- + `<svg xmlns="${ XML_NAME_SPACE }" viewBox="0 0 ${ SIZE } ${ SIZE }" width="${ SIZE }" height="${ SIZE }">`
- + `<path d="${ this.options.arrowPath || PATH }" />`
- + `</svg>`
- + `</button>`;
- }
- /**
- * Returns the HTML of the slider.
- *
- * @return The generated HTML.
- */
- html(): string {
- const { rootClass, listTag, arrows, beforeTrack, afterTrack } = this.config;
- let html = '';
- html += `<div id="${ this.id }" class="${ this.buildClasses() } ${ rootClass || '' }">`;
- html += `<style>${ this.Style.build() }</style>`;
- html += beforeTrack || '';
- html += `<div class="splide__track">`;
- html += `<${ listTag } class="splide__list">`;
- html += this.renderSlides();
- html += `</${ listTag }>`;
- html += `</div>`;
- if ( arrows ) {
- html += this.renderArrows();
- }
- html += afterTrack || '';
- html += `</div>`;
- return html;
- }
- }
|