import { MEDIA_PREFERS_REDUCED_MOTION } from '../../constants/media'; import { CREATED, DESTROYED } from '../../constants/states'; import { EventBinder } from '../../constructors'; import { Splide } from '../../core/Splide/Splide'; import { BaseComponent, Components, Options } from '../../types'; import { merge, omit, ownKeys } from '../../utils'; import { EVENT_UPDATED } from '../../constants/events'; /** * The interface for the Media component. * * @since 4.0.0 */ export interface MediaComponent extends BaseComponent { /** @internal */ reduce( reduced: boolean ): void; set( options: Options, base?: boolean, notify?: boolean ): void; } /** * The component for observing media queries and updating options if necessary. * This used to be the Options component. * * @since 4.0.0 * * @param Splide - A Splide instance. * @param Components - A collection of components. * @param options - Options. * * @return A Media component object. */ export function Media( Splide: Splide, Components: Components, options: Options ): MediaComponent { const { state } = Splide; const breakpoints = options.breakpoints || {}; const reducedMotion = options.reducedMotion || {}; const binder = EventBinder(); /** * Stores options and MediaQueryList object. */ const queries: Array<[ Options, MediaQueryList ]> = []; /** * Called when the component is constructed. */ function setup(): void { const isMin = options.mediaQuery === 'min'; ownKeys( breakpoints ) .sort( ( n, m ) => isMin ? +n - +m : +m - +n ) .forEach( key => { register( breakpoints[ key ], `(${ isMin ? 'min' : 'max' }-width:${ key }px)` ); } ); register( reducedMotion, MEDIA_PREFERS_REDUCED_MOTION ); update(); } /** * Destroys the component. * * @param completely - Will be `true` for complete destruction. */ function destroy( completely: boolean ): void { if ( completely ) { binder.destroy(); } } /** * Registers entries as [ Options, media query string ]. * * @param options - Options merged to current options when the document matches the query. * @param query - A query string. */ function register( options: Options, query: string ): void { const queryList = matchMedia( query ); binder.bind( queryList, 'change', update ); queries.push( [ options, queryList ] ); } /** * Checks all media queries in entries and updates options. */ function update(): void { const destroyed = state.is( DESTROYED ); const direction = options.direction; const merged = queries.reduce( ( merged, entry ) => { return merge( merged, entry[ 1 ].matches ? entry[ 0 ] : {} ); }, {} ); omit( options ); set( merged ); if ( options.destroy ) { Splide.destroy( options.destroy === 'completely' ); } else if ( destroyed ) { destroy( true ); Splide.mount(); } else { direction !== options.direction && Splide.refresh(); } } /** * Disables or enables `reducedMotion` options. * This method does nothing when the document does not match the query. * * @internal * * @param enable - Determines whether to apply `reducedMotion` options or not. */ function reduce( enable: boolean ): void { if ( matchMedia( MEDIA_PREFERS_REDUCED_MOTION ).matches ) { enable ? merge( options, reducedMotion ) : omit( options, ownKeys( reducedMotion ) ); } } /** * Sets current options or base options (prototype). * If changing base options, always emits the `updated` event. * * @internal * * @param opts - New options. * @param base - Optional. Determines whether to also update base options or not. * @param notify - Optional. If `true`, always emits the `update` event. */ function set( opts: Options, base?: boolean, notify?: boolean ): void { merge( options, opts ); base && merge( Object.getPrototypeOf( options ), opts ); if ( notify || ! state.is( CREATED ) ) { Splide.emit( EVENT_UPDATED, options ); } } return { setup, destroy, reduce, set, }; }