UNPKG

accordion-slider-js

Version:

Modular accordion slider built in Vanilla JavaScript.

1,033 lines (816 loc) 43.1 kB
import defaults from './accordion-slider-defaults.js'; import { resolveUnit } from '../helpers/util.js'; import AccordionSliderPanel from './accordion-slider-panel.js'; import CustomEventTarget from '../helpers/custom-event-target.js'; import WindowResizeHandler from '../helpers/window-resize-handler.js'; import AddOnsManager from '../add-ons/add-ons-manager.js'; class AccordionSlider extends CustomEventTarget { // The namespace to be used when adding event listeners namespace = 'accordionslider'; // Holds the final settings of the accordion after merging the specified // ones with the default ones. settings = {}; // Selector for the main element of the accordion selector; // reference to main accordion element accordionEl = null; // reference to the container of the panels panelsEl = null; // reference to the container that will mask the panels panelsMaskEl = null; // the index of the currently opened panel (starts with 0) currentIndex = -1; // the index of the current page currentPage = 0; // the size, in pixels, of the accordion totalSize = 0; // the size of the panels' container totalPanelsSize = 0; // the computed size, in pixels, of the opened panel computedOpenedPanelSize = 0; // the computed maximum allowed size, in pixels, of the opened panel maxComputedOpenedPanelSize = 0; // the size, in pixels, of the collapsed panels collapsedPanelSize = 0; // the size, in pixels, of the closed panels closedPanelSize = 0; // the distance, in pixels, between the accordion's panels computedPanelDistance = 0; // array that contains the AccordionSliderPanel objects panels = []; // timer used for delaying the opening of the panel on mouse hover mouseDelayTimer = 0; // simple objects to be used for animation openPanelAnimation = {}; closePanelsAnimation = {}; // keeps a reference to the previous number of visible panels previousVisiblePanels = -1; // indicates whether the accordion is currently scrolling isPageScrolling = false; // keeps a reference to the ratio between the size actual size of the accordion and the set size autoResponsiveRatio = 1; // indicates whether the panels will overlap, based on the set panelOverlap property // and also based on the computed distance between panels isOverlapping = false; // Stores references the event handlers in pairs containing the event identifier and the event handler // in order to be able to retrieve them when they need to be removed eventHandlerReferences = {}; // Reference to the WindowResizeHandler instance windowResizeHandler; // Reference to the AddOnsManager instance addOnsManager; constructor( selector, options = null ) { super(); this.selector = selector; this.settings = options !== null ? { ...defaults, ...options } : { ...defaults }; this.addOnsManager = new AddOnsManager( this, this.settings.addOns ); this.addOnsManager.init(); this.init(); } // The starting place for the accordion slider init() { this.dispatchEvent( 'beforeInit' ); this.accordionEl = document.querySelector( this.selector ); // Remove the 'as-no-js' when the accordion's JavaScript code starts running this.accordionEl.classList.remove( 'as-no-js' ); // Set up the panels containers // accordion-slider > as-mask > as-panels > as-panel this.panelsMaskEl = document.createElement( 'div' ); this.panelsMaskEl.classList.add( 'as-mask' ); this.accordionEl.appendChild( this.panelsMaskEl ); this.panelsContainerEl = this.accordionEl.getElementsByClassName( 'as-panels' )[ 0 ]; if ( this.accordionEl.getElementsByClassName( 'as-panels' ).length === 0 ) { this.panelsContainerEl = document.createElement( 'div' ); this.panelsContainerEl.classList.add( 'as-panels' ); } this.panelsMaskEl.appendChild( this.panelsContainerEl ); if ( this.settings.shuffle === true ) { const panels = Array.from( this.panelsContainerEl.getElementsByClassName( 'as-panel' ) ); const shuffledPanels = [ ...panels ]; for ( let k = shuffledPanels.length - 1; k > 0; k-- ) { let l = Math.floor( Math.random() * ( k + 1 ) ); let temp = shuffledPanels[ k ]; shuffledPanels[ k ] = shuffledPanels[ l ]; shuffledPanels[ l ] = temp; } this.panelsContainerEl.replaceChildren( ...shuffledPanels ); } // set a panel to be opened from the start this.currentIndex = this.settings.startPanel; if ( this.currentIndex === -1 ) { this.accordionEl.classList.add( 'as-closed' ); } else { this.accordionEl.classList.add( 'as-opened' ); } // if a panels was not set to be opened but a page was specified, // set that page index to be opened if ( this.settings.startPage !== -1 ) { this.currentPage = this.settings.startPage; } this.windowResizeHandler = new WindowResizeHandler(); this.windowResizeHandler.addEventListener( 'resize', () => { this.resize(); }); this.update(); // if there is a panel opened at start handle that panel as if it was manually opened if ( this.currentIndex !== -1 ) { this.accordionEl.getElementsByClassName( 'as-panel' )[ this.currentIndex ].classList.add( 'as-opened' ); this.dispatchEvent( 'panelOpen', { index: this.currentIndex, previousIndex: -1 } ); } // listen for 'mouseenter' events this.accordionEl.addEventListener( 'mouseenter', this.eventHandlerReferences[ 'mouseenter.accordion' ] = () => { this.dispatchEvent( 'accordionMouseOver' ); }); // listen for 'mouseleave' events this.accordionEl.addEventListener( 'mouseleave', this.eventHandlerReferences[ 'mouseleave.accordion' ] = () => { clearTimeout( this.mouseDelayTimer ); // close the panels if ( this.settings.closePanelsOnMouseOut === true ) { this.closePanels(); } this.dispatchEvent( 'accordionMouseOut' ); }); this.dispatchEvent( 'init' ); } update() { this.dispatchEvent( 'beforeUpdate' ); // add a class to the accordion based on the orientation // to be used in CSS if ( this.settings.orientation === 'horizontal' ) { this.accordionEl.classList.remove( 'as-vertical' ); this.accordionEl.classList.add( 'as-horizontal' ); } else if (this.settings.orientation === 'vertical') { this.accordionEl.classList.remove( 'as-horizontal' ); this.accordionEl.classList.add( 'as-vertical' ); } // if the number of visible panels has change, update the current page to reflect // the same relative position of the panels if ( this.settings.visiblePanels === -1 ) { this.currentPage = 0; } else if ( this.currentIndex !== -1 ) { this.currentPage = Math.floor( this.currentIndex / this.settings.visiblePanels ); } else if ( this.settings.visiblePanels !== this.previousVisiblePanels && this.previousVisiblePanels !== -1 ) { const correctPage = Math.round( ( this.currentPage * this.previousVisiblePanels ) / this.settings.visiblePanels ); if ( this.currentPage !== correctPage ) { this.currentPage = correctPage; } this.previousVisiblePanels = this.settings.visiblePanels; } // if there is distance between the panels, the panels can't overlap if ( this.settings.panelDistance > 0 || this.settings.panelOverlap === false ) { this.isOverlapping = false; this.accordionEl.classList.remove( 'as-overlap' ); } else if ( this.settings.panelOverlap === true ) { this.isOverlapping = true; this.accordionEl.classList.add( 'as-overlap' ); } // clear inline size of the background images because the orientation might have changes [ ...Array.from( this.accordionEl.getElementsByClassName( 'as-background' ) ), ...Array.from( this.accordionEl.getElementsByClassName( 'as-background-opened' ) ) ].forEach( ( imageEl ) => { imageEl.style.removeProperty( 'width' ); imageEl.style.removeProperty( 'height' ); }); // update panels this.updatePanels(); // create or remove the shadow if ( this.settings.shadow === true ) { Array.from( this.accordionEl.getElementsByClassName( 'as-panel' ) ).forEach( ( panelEl ) => { panelEl.classList.add( 'as-shadow' ); }); } else if ( this.settings.shadow === false ) { Array.from( this.accordionEl.getElementsByClassName( 'as-shadow' ) ).forEach( ( panelEl ) => { panelEl.classList.remove( 'as-shadow' ); }); } // reset the panels' container position this.panelsContainerEl.removeAttribute( 'style' ); // set the size of the accordion this.resize(); // fire the update event this.dispatchEvent( 'update' ); } /* Called when the accordion needs to resize */ resize() { this.dispatchEvent( 'beforeResize' ); const positionProperty = this.settings.orientation === 'horizontal' ? 'left' : 'top'; const sizeProperty = this.settings.orientation === 'horizontal' ? 'width' : 'height'; this.panelsMaskEl.removeAttribute( 'style' ); // prepare the accordion for responsiveness if ( this.settings.responsive === true ) { // if the accordion is responsive set the width to 100% and use // the specified width and height as a max-width and max-height this.accordionEl.style.width = '100%'; this.accordionEl.style.height = resolveUnit( this.settings.height ); this.accordionEl.style.maxWidth = resolveUnit( this.settings.width ); this.accordionEl.style.maxHeight = resolveUnit( this.settings.height ); // if an aspect ratio was not specified, set the aspect ratio // based on the specified width and height if ( this.settings.aspectRatio === -1 ) { this.settings.aspectRatio = this.settings.width / this.settings.height; } this.accordionEl.style.height = resolveUnit( this.accordionEl.clientWidth / this.settings.aspectRatio ); if ( this.settings.responsiveMode === 'auto' ) { // get the accordion's size ratio based on the set size and the actual size this.autoResponsiveRatio = this.accordionEl.clientWidth / this.settings.width; this.panelsMaskEl.style.width = resolveUnit( this.settings.width ); if ( isNaN( this.settings.height ) ) { this.panelsMaskEl.style.height = resolveUnit( Math.min( this.settings.width / this.settings.aspectRatio, parseInt( this.settings.height, 10 ) / 100 * window.innerHeight ) ); } else { this.panelsMaskEl.style.height = resolveUnit( Math.min( this.settings.width / this.settings.aspectRatio, this.settings.height ) ); } // scale the mask container based on the current ratio if ( this.autoResponsiveRatio < 1 ) { this.panelsMaskEl.style.transform = `scaleX(${ this.autoResponsiveRatio }) scaleY(${ this.autoResponsiveRatio })`; this.panelsMaskEl.style.transformOrigin = 'top left'; } else { this.panelsMaskEl.style.removeProperty( 'transform' ); this.panelsMaskEl.style.removeProperty( 'transform-origin' ); } } this.totalSize = this.settings.orientation === 'horizontal' ? this.panelsMaskEl.clientWidth : this.panelsMaskEl.clientHeight; } else { this.accordionEl.style.width = resolveUnit( this.settings.width ); this.accordionEl.style.height = resolveUnit( this.settings.height ); this.accordionEl.style.removeProperty( 'max-width' ); this.accordionEl.style.removeProperty( 'max-height' ); this.totalSize = this.settings.orientation === 'horizontal' ? this.accordionEl.clientWidth : this.accordionEl.clientHeight; } // set the size of the background images explicitly because of a bug? // that causes anchors not to adapt their size to the size of the image, // when the image size is set in percentages, which causes the total size // of the panel to be bigger than it should [ ...Array.from( this.accordionEl.getElementsByClassName( 'as-background' ) ), ...Array.from( this.accordionEl.getElementsByClassName( 'as-background-opened' ) ) ].forEach( ( imageEl ) => { if ( this.settings.orientation === 'horizontal' ) { imageEl.style.height = resolveUnit( this.panelsContainerEl.clientHeight ); } else { imageEl.style.width = resolveUnit( this.panelsContainerEl.clientWidth ); } }); // set the initial computedPanelDistance to the value defined in the options this.computedPanelDistance = this.settings.panelDistance; // parse computedPanelDistance and set it to a pixel value if ( typeof this.computedPanelDistance === 'string' ) { if ( this.computedPanelDistance.indexOf( '%' ) !== -1 ) { this.computedPanelDistance = this.totalSize * ( parseInt( this.computedPanelDistance, 10 ) / 100 ); } else if ( this.computedPanelDistance.indexOf( 'px' ) !== -1 ) { this.computedPanelDistance = parseInt( this.computedPanelDistance, 10 ); } } // set the size, in pixels, of the closed panels this.closedPanelSize = ( this.totalSize - ( this.getVisiblePanels() - 1 ) * this.computedPanelDistance ) / this.getVisiblePanels(); // round the value this.closedPanelSize = Math.floor( this.closedPanelSize ); // set the initial computedOpenedPanelSize to the value defined in the options this.computedOpenedPanelSize = this.settings.openedPanelSize; // if the panels are set to open to their maximum size, // parse maxComputedOpenedPanelSize and set it to a pixel value if ( this.settings.openedPanelSize === 'max' ) { // set the initial maxComputedOpenedPanelSize to the value defined in the options this.maxComputedOpenedPanelSize = this.settings.maxOpenedPanelSize; if ( typeof this.maxComputedOpenedPanelSize === 'string' ) { if ( this.maxComputedOpenedPanelSize.indexOf( '%' ) !== -1 ) { this.maxComputedOpenedPanelSize = this.totalSize * ( parseInt( this.maxComputedOpenedPanelSize, 10 ) / 100 ); } else if (this.maxComputedOpenedPanelSize.indexOf( 'px' ) !== -1 ) { this.maxComputedOpenedPanelSize = parseInt( this.maxComputedOpenedPanelSize, 10 ); } } } // parse computedOpenedPanelSize and set it to a pixel value if ( typeof this.computedOpenedPanelSize === 'string' ) { if ( this.computedOpenedPanelSize.indexOf( '%' ) !== -1 ) { this.computedOpenedPanelSize = this.totalSize * ( parseInt( this.computedOpenedPanelSize, 10 ) / 100 ); } else if ( this.computedOpenedPanelSize.indexOf( 'px' ) !== -1 ) { this.computedOpenedPanelSize = parseInt( this.computedOpenedPanelSize, 10 ); } else if ( this.computedOpenedPanelSize === 'max' && this.currentIndex !== -1 ) { const contentSize = this.getPanelAt( this.currentIndex ).getContentSize(); this.computedOpenedPanelSize = contentSize === 'loading' ? this.closedPanelSize : Math.min( contentSize, this.maxComputedOpenedPanelSize ); } } // set the size, in pixels, of the collapsed panels this.collapsedPanelSize = ( this.totalSize - this.computedOpenedPanelSize - ( this.getVisiblePanels() - 1 ) * this.computedPanelDistance ) / ( this.getVisiblePanels() - 1 ); // round the values this.computedOpenedPanelSize = Math.floor( this.computedOpenedPanelSize ); this.collapsedPanelSize = Math.floor( this.collapsedPanelSize ); // get the total size of the panels' container this.totalPanelsSize = this.closedPanelSize * this.getTotalPanels() + this.computedPanelDistance * ( this.getTotalPanels() - 1 ); this.panelsContainerEl.style[ sizeProperty ] = resolveUnit( this.totalPanelsSize ); // recalculate the totalSize due to the fact that rounded sizes can cause incorrect positioning // since the actual size of all panels from a page might be smaller than the whole width of the accordion this.totalSize = this.closedPanelSize * this.getVisiblePanels() + this.computedPanelDistance * ( this.getVisiblePanels() - 1 ); if ( this.settings.responsiveMode === 'custom' || this.settings.responsive === false ) { this.accordionEl.style[ sizeProperty ] = resolveUnit( this.totalSize ); } else { this.accordionEl.style[ sizeProperty ] = resolveUnit( this.totalSize * this.autoResponsiveRatio ); this.panelsMaskEl.style[ sizeProperty ] = resolveUnit( this.totalSize ); } // if there are multiple pages, set the correct position of the panels' container if ( this.settings.visiblePanels !== -1 ) { let targetPosition = - ( this.totalSize + this.computedPanelDistance ) * this.currentPage; if ( this.currentPage === this.getTotalPages() - 1 ) { targetPosition = - ( this.closedPanelSize * this.getTotalPanels() + this.computedPanelDistance * ( this.getTotalPanels() - 1 ) - this.totalSize ); } this.panelsContainerEl.style[ positionProperty ] = resolveUnit( targetPosition ); } // calculate missing panels for the last page of panels const missingPanels = ( this.currentPage === this.getTotalPages() - 1 ) && ( this.getTotalPanels() % this.settings.visiblePanels ) !== 0 ? this.settings.visiblePanels - this.getTotalPanels() % this.settings.visiblePanels : 0; // set the position and size of each panel this.panels.forEach( ( panel, index ) => { // get the position of the panel based on the currently opened index and the panel's index let position; if ( this.currentIndex === -1 ) { position = index * ( this.closedPanelSize + this.computedPanelDistance ); } else if ( this.settings.visiblePanels === -1 ) { position = index * ( this.collapsedPanelSize + this.computedPanelDistance ) + ( index > this.currentIndex ? this.computedOpenedPanelSize - this.collapsedPanelSize : 0 ); } else { if ( this.getPageOfPanel( index ) === this.currentPage ) { position = this.currentPage * ( this.totalSize + this.computedPanelDistance ) + ( index + missingPanels - this.currentPage * this.settings.visiblePanels ) * ( this.collapsedPanelSize + this.computedPanelDistance ) + ( index > this.currentIndex ? this.computedOpenedPanelSize - this.collapsedPanelSize : 0 ); if ( this.currentPage === this.getTotalPages() - 1 && missingPanels !== 0 ) position -= ( this.getTotalPages() - this.getTotalPanels() / this.settings.visiblePanels ) * ( this.totalSize + this.computedPanelDistance ); } else { position = index * ( this.closedPanelSize + this.computedPanelDistance ); } } panel.setPosition( position ); // get the size of the panel based on the state of the panel (opened, closed or collapsed) if ( this.isOverlapping === false ) { const size = ( this.currentIndex === -1 || ( this.settings.visiblePanels !== -1 && this.getPageOfPanel( index ) !== this.currentPage ) ) ? this.closedPanelSize : ( index === this.currentIndex ? this.computedOpenedPanelSize : this.collapsedPanelSize ); panel.setSize( size ); } }); this.dispatchEvent( 'resize' ); } /* Create, remove or update panels based on the HTML specified in the accordion */ updatePanels() { // check if there are removed items in the DOM and remove the from the array of panels [ ...this.panels ].forEach( ( panel, index ) => { if ( this.accordionEl.querySelector( `.as-panel[data-index="${ index }"]` ) === null ) { panel.removeEventListener( 'panelMouseOver' ); panel.removeEventListener( 'panelMouseOut' ); panel.removeEventListener( 'panelClick' ); panel.removeEventListener( 'imagesComplete'); panel.destroy(); const indexOfPanel = this.panels.findIndex( panel => panel.index === index ); this.panels.splice( indexOfPanel, 1 ); } }); // parse the DOM and create un-instantiated panels and reset the indexes Array.from( this.accordionEl.getElementsByClassName( 'as-panel' ) ).forEach( ( panelEl, index ) => { if ( panelEl.hasAttribute( 'data-init' ) === false ) { const panel = this.createPanel( panelEl ); this.panels.splice( index, 0, panel ); } this.panels[ index ].settings = this.settings; this.panels[ index ].index = index; panelEl.style.removeProperty( 'top' ); panelEl.style.removeProperty( 'left' ); panelEl.style.removeProperty( 'width' ); panelEl.style.removeProperty( 'height' ); }); } /* Create an individual panel */ createPanel( panelEl ) { // create a panel instance and add it to the array of panels const panel = new AccordionSliderPanel( panelEl, this.settings ); // listen for 'panelMouseOver' events panel.addEventListener( 'panelMouseOver', ( event ) => { if ( this.isPageScrolling === true ) { return; } if ( this.settings.openPanelOn === 'hover' ) { clearTimeout( this.mouseDelayTimer ); // open the panel, but only after a short delay in order to prevent // opening panels that the user doesn't intend this.mouseDelayTimer = setTimeout( () => { this.openPanel( event.detail.index ); }, this.settings.mouseDelay ); } this.dispatchEvent( 'panelMouseOver', { index: event.detail.index } ); }); // listen for 'panelMouseOut' events panel.addEventListener( 'panelMouseOut', ( event ) => { if ( this.isPageScrolling === true ) { return; } this.dispatchEvent( 'panelMouseOut', { index: event.detail.index } ); }); // listen for 'panelClick' events panel.addEventListener( 'panelClick', ( event ) => { if ( this.accordionEl.classList.contains( 'as-swiping' ) ) { return; } if ( this.settings.openPanelOn === 'click' ) { // open the panel if it's not already opened // and close the panels if the clicked panel is opened if ( event.detail.index !== this.currentIndex ) { this.openPanel( event.detail.index ); } else { this.closePanels(); } } this.dispatchEvent( 'panelClick', { index: event.detail.index } ); }); // disable links if the panel should open on click and it wasn't opened yet panel.addEventListener( 'panelMouseDown', ( event ) => { const links = panelEl.getElementsByTagName( 'a' ); if ( links.length < 1 ) { return; } Array.from( links ).forEach( ( linkEl ) => { linkEl.removeEventListener( 'click', this.eventHandlerReferences[ 'click.link.panel' ] ); }); if ( event.detail.index !== this.currentIndex && this.settings.openPanelOn === 'click' ) { Array.from( links ).forEach( ( linkEl ) => { linkEl.addEventListener( 'click', this.eventHandlerReferences[ 'click.link.panel' ] = ( event ) => { event.preventDefault(); }); }); } }); // listen for 'imagesComplete' events and if the images were loaded in // the panel that is currently opened and the size of the panel is different // than the currently computed size of the panel, force the re-opening of the panel // to the correct size panel.addEventListener( 'imagesComplete', ( event ) => { if ( event.detail.index === this.currentIndex && event.detail.contentSize !== this.computedOpenedPanelSize ) { this.openPanel( event.detail.index, true ); } }); return panel; } /* Return the panel at the specified index */ getPanelAt( index ) { return this.panels[ index ]; } /* Return the index of the currently opened panel */ getCurrentIndex() { return this.currentIndex; } /* Return the total amount of panels */ getTotalPanels() { return this.panels.length; } /* Open the next panel */ nextPanel() { const index = ( this.currentIndex >= this.getTotalPanels() - 1 ) ? 0 : ( this.currentIndex + 1 ); this.openPanel( index ); } /* Open the previous panel */ previousPanel() { const index = this.currentIndex <= 0 ? (this.getTotalPanels() - 1 ) : ( this.currentIndex - 1 ); this.openPanel( index ); } /* Destroy the Accordion Slider instance */ destroy() { this.addOnsManager.destroyAll(); // remove inline style this.accordionEl.removeAttribute( 'style' ); this.panelsContainerEl.removeAttribute( 'style' ); this.accordionEl.setAttribute( 'class', 'accordion-slider as-no-js' ); // detach event handlers this.accordionEl.removeEventListener( 'mouseenter', this.eventHandlerReferences[ 'mouseenter.accordion' ] ); this.accordionEl.removeEventListener( 'mouseleave', this.eventHandlerReferences[ 'mouseleave.accordion' ] ); this.windowResizeHandler.removeEventListener( 'resize' ); this.windowResizeHandler.destroy(); // stop animations this.stopPanelsAnimation( this.openPanelAnimation ); this.stopPanelsAnimation( this.closePanelsAnimation ); // destroy all panels this.panels.forEach( ( panel ) => { panel.removeEventListener( 'panelMouseOver' ); panel.removeEventListener( 'panelMouseOut' ); panel.removeEventListener( 'panelClick' ); panel.removeEventListener( 'imagesComplete' ); panel.destroy(); }); this.panels.length = 0; // move the panels from the mask container back in the main accordion container this.accordionEl.insertBefore( this.panelsContainerEl, this.accordionEl.firstChild ); // remove elements that were created by the script this.panelsMaskEl.remove(); } /* Animate the panels using request animation frame */ animatePanels( target, args ) { const startTime = new Date().valueOf(); let progress = 0; target.isRunning = true; target.timer = window.requestAnimationFrame( animate ); function animate() { if ( progress < 1 ) { // get the progress by calculating the elapsed time progress = ( new Date().valueOf() - startTime ) / args.duration; if ( progress > 1 ) { progress = 1; } // apply swing easing progress = 0.5 - Math.cos( progress * Math.PI ) / 2; args.step( progress ); target.timer = window.requestAnimationFrame( animate ); } else { args.complete(); target.isRunning = false; window.cancelAnimationFrame( target.timer ); } } } /* Stop running panel animations */ stopPanelsAnimation( target ) { if ( typeof target.isRunning !== 'undefined' && target.isRunning === true ) { target.isRunning = false; window.cancelAnimationFrame( target.timer ); } } /* Open the panel at the specified index */ openPanel( index, force ) { if ( index === this.currentIndex && force !== true ) { return; } // remove the "closed" class and add the "opened" class, which indicates // that the accordion has an opened panel if ( this.accordionEl.classList.contains( 'as-closed' ) === true ) { this.accordionEl.classList.replace( 'as-closed', 'as-opened' ); } const previousIndex = this.currentIndex; this.currentIndex = index; // synchronize the page with the selected panel by navigating to the page that // contains the panel if necessary. // if the last page is already selected and the selected panel is on this last page // don't navigate to a different page no matter what panel is selected and whether // the panel actually belongs to the previous page if ( this.settings.visiblePanels !== -1 && ! ( this.currentPage === this.getTotalPages() - 1 && index >= this.getTotalPanels() - this.settings.visiblePanels ) ) { const page = Math.floor( this.currentIndex / this.settings.visiblePanels ); if ( page !== this.currentPage ) { this.gotoPage( page ); } // reset the current index because when the closePanels was called inside gotoPage the current index became -1 this.currentIndex = index; } const targetSize = [], targetPosition = [], startSize = [], startPosition = [], animatedPanels = [], firstPanel = this.getFirstPanelFromPage(), lastPanel = this.getLastPanelFromPage(); let counter = 0; if ( this.accordionEl.querySelector( '.as-panel.as-opened' ) !== null ) { this.accordionEl.querySelector( '.as-panel.as-opened' ).classList.remove( 'as-opened' ); } this.accordionEl.getElementsByClassName( 'as-panel' )[ this.currentIndex ].classList.add( 'as-opened' ); // check if the panel needs to open to its maximum size and recalculate // the size of the opened panel and the size of the collapsed panel if ( this.settings.openedPanelSize === 'max' ) { const contentSize = this.getPanelAt( this.currentIndex ).getContentSize(); this.computedOpenedPanelSize = contentSize === 'loading' ? this.closedPanelSize : Math.min( contentSize, this.maxComputedOpenedPanelSize ); this.collapsedPanelSize = ( this.totalSize - this.computedOpenedPanelSize - ( this.getVisiblePanels() - 1 ) * this.computedPanelDistance ) / ( this.getVisiblePanels() - 1 ); } // get the starting and target position and size of each panel for ( let i = firstPanel; i <= lastPanel; i++ ) { let panel = this.getPanelAt( i ); startPosition[ i ] = panel.getPosition(); targetPosition[ i ] = this.currentPage * ( this.totalSize + this.computedPanelDistance ) + counter * ( this.collapsedPanelSize + this.computedPanelDistance ) + ( i > this.currentIndex ? this.computedOpenedPanelSize - this.collapsedPanelSize : 0 ); // the last page might contain less panels than the set number of visible panels. // in this situation, the last page will contain some panels from the previous page // and this requires the panels from the last page to be positioned differently than // the rest of the panels. this requires some amendments to the position of the last panels // by replacing the current page index with a float number: this.getTotalPanels() / this.settings.visiblePanels, // which would represent the actual number of existing pages. // here we subtract the float number from the formal number of pages in order to calculate // how much length it's necessary to subtract from the initially calculated value if ( this.settings.visiblePanels !== -1 && this.currentPage === this.getTotalPages() - 1 ) { targetPosition[ i ] -= ( this.getTotalPages() - this.getTotalPanels() / this.settings.visiblePanels ) * ( this.totalSize + this.computedPanelDistance ); } // check if the panel's position needs to change if ( targetPosition[ i ] !== startPosition[ i ] ) { animatedPanels.push( i ); } if ( this.isOverlapping === false ) { startSize[ i ] = panel.getSize(); targetSize[ i ] = i === this.currentIndex ? this.computedOpenedPanelSize : this.collapsedPanelSize; // check if the panel's size needs to change if ( targetSize[ i ] !== startSize[ i ] && animatedPanels.indexOf( i ) === -1 ) animatedPanels.push( i ); } counter++; } const totalPanels = animatedPanels.length; // stop the close panels animation if it's on the same page if ( this.closePanelsAnimation.page === this.currentPage ) { this.stopPanelsAnimation( this.closePanelsAnimation ); } // stop any running animations this.stopPanelsAnimation( this.openPanelAnimation ); // assign the current page this.openPanelAnimation.page = this.currentPage; // animate the panels this.animatePanels( this.openPanelAnimation, { duration: this.settings.openPanelDuration, step: ( progress ) => { for ( let i = 0; i < totalPanels; i++ ) { const value = animatedPanels[ i ], panel = this.getPanelAt( value ); panel.setPosition( progress * ( targetPosition[ value ] - startPosition[ value ] ) + startPosition[ value ] ); if ( this.isOverlapping === false ) { panel.setSize( progress * ( targetSize[ value ] - startSize[ value ] ) + startSize[ value ] ); } } }, complete: () => { // fire 'panelOpenComplete' event this.dispatchEvent( 'panelOpenComplete', { index: this.currentIndex } ); } }); this.dispatchEvent( 'panelOpen', { index: this.currentIndex, previousIndex: previousIndex } ); } /* Close the panels */ closePanels() { const previousIndex = this.currentIndex; this.currentIndex = -1; // remove the "opened" class and add the "closed" class, which indicates // that the accordion is closed if ( this.accordionEl.classList.contains( 'as-opened' ) === true ) { this.accordionEl.classList.replace( 'as-opened', 'as-closed' ); } // remove the "opened" class from the previously opened panel if ( this.accordionEl.querySelector( '.as-panel.as-opened' ) !== null ) { this.accordionEl.querySelector( '.as-panel.as-opened' ).classList.remove( 'as-opened' ); } clearTimeout( this.mouseDelayTimer ); const targetSize = [], targetPosition = [], startSize = [], startPosition = [], firstPanel = this.getFirstPanelFromPage(), lastPanel = this.getLastPanelFromPage(); let counter = 0; // get the starting and target size and position of each panel for ( let i = firstPanel; i <= lastPanel; i++ ) { const panel = this.getPanelAt( i ); startPosition[ i ] = panel.getPosition(); targetPosition[ i ] = this.currentPage * ( this.totalSize + this.computedPanelDistance ) + counter * ( this.closedPanelSize + this.computedPanelDistance ); // same calculations as in openPanel if ( this.settings.visiblePanels !== -1 && this.currentPage === this.getTotalPages() - 1 ) { targetPosition[ i ] -= ( this.getTotalPages() - this.getTotalPanels() / this.settings.visiblePanels ) * ( this.totalSize + this.computedPanelDistance ); } if ( this.isOverlapping === false ) { startSize[ i ] = panel.getSize(); targetSize[ i ] = this.closedPanelSize; } counter++; } // stop the open panel animation if it's on the same page if ( this.openPanelAnimation.page === this.currentPage ) { this.stopPanelsAnimation( this.openPanelAnimation ); } // stop any running animations this.stopPanelsAnimation( this.closePanelsAnimation ); // assign the current page this.closePanelsAnimation.page = this.currentPage; // animate the panels this.animatePanels( this.closePanelsAnimation, { duration: this.settings.closePanelDuration, step: ( progress ) => { for ( let i = firstPanel; i <= lastPanel; i++ ) { const panel = this.getPanelAt( i ); panel.setPosition( progress * ( targetPosition[ i ] - startPosition[ i ] ) + startPosition[ i ] ); if ( this.isOverlapping === false ) { panel.setSize( progress * ( targetSize[ i ] - startSize[ i ] ) + startSize[ i ] ); } } }, complete: () => { // fire 'panelsCloseComplete' event this.dispatchEvent( 'panelsCloseComplete', { previousIndex: previousIndex } ); } }); // fire 'panelsClose' event this.dispatchEvent( 'panelsClose', { previousIndex: previousIndex } ); } /* Return the number of visible panels */ getVisiblePanels() { return this.settings.visiblePanels === -1 ? this.getTotalPanels() : this.settings.visiblePanels; } /* Return the total number of pages */ getTotalPages() { if ( this.settings.visiblePanels === -1 ) { return 1; } return Math.ceil( this.getTotalPanels() / this.settings.visiblePanels ); } /* Return the current page */ getCurrentPage() { return this.settings.visiblePanels === -1 ? 0 : this.currentPage; } /* Navigate to the indicated page */ gotoPage( index ) { // close any opened panels before scrolling to a different page if ( this.currentIndex !== -1 ) { this.closePanels(); } this.currentPage = index; this.isPageScrolling = true; const positionProperty = this.settings.orientation === 'horizontal' ? 'left' : 'top'; let targetPosition = - ( index * this.totalSize + this.currentPage * this.computedPanelDistance ); if ( this.currentPage === this.getTotalPages() - 1 ) { targetPosition = - ( this.totalPanelsSize - this.totalSize ); } // fire 'pageScroll' event this.dispatchEvent( 'pageScroll', { index: this.currentPage } ); this.panelsContainerEl.addEventListener( 'transitionend', () => { this.isPageScrolling = false; // fire 'pageScrollComplete' event this.dispatchEvent( 'pageScrollComplete', { index: this.currentPage } ); }); this.panelsContainerEl.style.transition = `${ positionProperty } ${ this.settings.pageScrollDuration / 1000 }s`; this.panelsContainerEl.style[ positionProperty ] = resolveUnit( targetPosition ); } /* Navigate to the next page */ nextPage() { const index = this.currentPage >= this.getTotalPages() - 1 ? 0 : this.currentPage + 1; this.gotoPage( index ); } /* Navigate to the previous page */ previousPage() { const index = this.currentPage <= 0 ? this.getTotalPages() - 1 : this.currentPage - 1; this.gotoPage( index ); } /* Calculate and return the first panel from the current page */ getFirstPanelFromPage() { if ( this.settings.visiblePanels === -1 ) { return 0; } else if ( this.currentPage === this.getTotalPages() - 1 && this.currentPage !== 0 ) { return this.getTotalPanels() - this.settings.visiblePanels; } else { return this.currentPage * this.settings.visiblePanels; } } /* Calculate and return the last panel from the current page */ getLastPanelFromPage() { if ( this.settings.visiblePanels === -1 ) { return this.getTotalPanels() - 1; } else if ( this.currentPage === this.getTotalPages() - 1 ) { return this.getTotalPanels() - 1; } else { return ( this.currentPage + 1 ) * this.settings.visiblePanels - 1; } } /* Return the page that the specified panel belongs to */ getPageOfPanel( index ) { if ( this.currentPage === this.getTotalPages() - 1 && index >= this.getTotalPanels() - this.settings.visiblePanels ) return this.getTotalPages() - 1; return Math.floor( index / this.settings.visiblePanels ); } } export default AccordionSlider;