import { queryOne, queryAll } from '@ecl/dom-utils';/** * @param {HTMLElement} element DOM element for component instantiation and scope * @param {Object} options * @param {String} options.toggleSelector Selector for toggling element * @param {String} options.prevSelector Selector for prev element * @param {String} options.nextSelector Selector for next element * @param {String} options.contentClass Selector for the content container * @param {String} options.slidesClass Selector for the slides container * @param {String} options.slideClass Selector for the slide items * @param {String} options.currentSlideClass Selector for the counter current slide number */export class NewsTicker { /** * @static * Shorthand for instance creation and initialisation. * * @param {HTMLElement} root DOM element for component instantiation and scope * * @return {NewsTicker} An instance of News ticker. */ static autoInit(root, { NEWS_TICKER: defaultOptions = {} } = {}) { const newsTicker = new NewsTicker(root, defaultOptions); newsTicker.init(); root.ECLNewsTicker = newsTicker; return newsTicker; } constructor( element, { playSelector = '[data-ecl-news-ticker-play]', pauseSelector = '[data-ecl-news-ticker-pause]', prevSelector = '[data-ecl-news-ticker-prev]', nextSelector = '[data-ecl-news-ticker-next]', containerClass = '.ecl-news-ticker__container', contentClass = '.ecl-news-ticker__content', slidesClass = '.ecl-news-ticker__slides', slideClass = '.ecl-news-ticker__slide', currentSlideClass = '.ecl-news-ticker__counter--current', controlsClass = '.ecl-news-ticker__controls', attachClickListener = true, attachResizeListener = true, } = {}, ) { // Check element if (!element || element.nodeType !== Node.ELEMENT_NODE) { throw new TypeError( 'DOM element should be given to initialize this widget.', ); } this.element = element; // Options this.playSelector = playSelector; this.pauseSelector = pauseSelector; this.prevSelector = prevSelector; this.nextSelector = nextSelector; this.containerClass = containerClass; this.contentClass = contentClass; this.slidesClass = slidesClass; this.slideClass = slideClass; this.currentSlideClass = currentSlideClass; this.controlsClass = controlsClass; this.attachClickListener = attachClickListener; this.attachResizeListener = attachResizeListener; // Private variables this.container = null; this.content = null; this.slides = null; this.btnPlay = null; this.btnPause = null; this.btnPrev = null; this.btnNext = null; this.index = 1; this.total = 0; this.allowShift = true; this.autoPlay = null; this.autoPlayInterval = null; this.hoverAutoPlay = null; this.resizeTimer = null; this.cloneFirstSLide = null; this.cloneLastSLide = null; // Bind `this` for use in callbacks this.handleAutoPlay = this.handleAutoPlay.bind(this); this.handleMouseOver = this.handleMouseOver.bind(this); this.handleMouseOut = this.handleMouseOut.bind(this); this.shiftSlide = this.shiftSlide.bind(this); this.checkIndex = this.checkIndex.bind(this); this.moveSlides = this.moveSlides.bind(this); this.handleResize = this.handleResize.bind(this); this.handleFocus = this.handleFocus.bind(this); } /** * Initialise component. */ init() { if (!ECL) { throw new TypeError('Called init but ECL is not present'); } ECL.components = ECL.components || new Map(); this.btnPlay = queryOne(this.playSelector, this.element); this.btnPause = queryOne(this.pauseSelector, this.element); this.btnPrev = queryOne(this.prevSelector, this.element); this.btnNext = queryOne(this.nextSelector, this.element); this.slidesContainer = queryOne(this.slidesClass, this.element); this.container = queryOne(this.containerClass, this.element); this.content = queryOne(this.contentClass, this.element); this.controls = queryOne(this.controlsClass, this.element); this.slides = queryAll(this.slideClass, this.element); this.total = this.slides.length; // If only one slide, don't initialize ticker and hide controls if (this.total <= 1 && this.controls) { this.content.style.height = 'auto'; this.controls.style.display = 'none'; return false; } const firstSlide = this.slides[0]; const lastSlide = this.slides[this.slides.length - 1]; this.cloneFirstSLide = firstSlide.cloneNode(true); this.cloneLastSLide = lastSlide.cloneNode(true); // Clone first and last slide this.slidesContainer.appendChild(this.cloneFirstSLide); this.slidesContainer.insertBefore(this.cloneLastSLide, firstSlide); // Refresh the slides variable after adding new cloned slides this.slides = queryAll(this.slideClass, this.element); // Initialize ticker position and size this.handleResize(); // Activate autoPlay this.handleAutoPlay(); // Bind events if (this.attachClickListener && this.btnPlay && this.btnPause) { this.btnPlay.addEventListener('click', this.handleAutoPlay); this.btnPause.addEventListener('click', this.handleAutoPlay); } if (this.attachClickListener && this.btnNext) { this.btnNext.addEventListener( 'click', this.shiftSlide.bind(this, 1, true), ); } if (this.attachClickListener && this.btnPrev) { this.btnPrev.addEventListener( 'click', this.shiftSlide.bind(this, -1, true), ); } if (this.slidesContainer) { this.slidesContainer.addEventListener('transitionend', this.checkIndex); this.slidesContainer.addEventListener('mouseover', this.handleMouseOver); this.slidesContainer.addEventListener('mouseout', this.handleMouseOut); } if (this.container) { this.container.addEventListener('focus', this.handleFocus, true); } if (this.attachResizeListener) { window.addEventListener('resize', this.handleResize); } // Set ecl initialized attribute this.element.setAttribute('data-ecl-auto-initialized', 'true'); ECL.components.set(this.element, this); return this; } /** * Destroy component. */ destroy() { if (this.cloneFirstSLide && this.cloneLastSLide) { this.cloneFirstSLide.remove(); this.cloneLastSLide.remove(); } if (this.btnPlay) { this.btnPlay.replaceWith(this.btnPlay.cloneNode(true)); } if (this.btnPause) { this.btnPause.replaceWith(this.btnPause.cloneNode(true)); } if (this.btnNext) { this.btnNext.replaceWith(this.btnNext.cloneNode(true)); } if (this.btnPrev) { this.btnPrev.replaceWith(this.btnPrev.cloneNode(true)); } if (this.slidesContainer) { this.slidesContainer.removeEventListener( 'transitionend', this.checkIndex, ); this.slidesContainer.removeEventListener( 'mouseover', this.handleMouseOver, ); this.slidesContainer.removeEventListener('mouseout', this.handleMouseOut); } if (this.container) { this.container.removeEventListener('focus', this.handleFocus, true); } if (this.attachResizeListener) { window.removeEventListener('resize', this.handleResize); } if (this.autoPlayInterval) { clearInterval(this.autoPlayInterval); this.autoPlay = null; } if (this.element) { this.element.removeAttribute('data-ecl-auto-initialized'); ECL.components.delete(this.element); } } /** * Action to shift next or previous slide. * @param {int} dir * @param {Boolean} stopAutoPlay */ shiftSlide(dir, stopAutoPlay) { if (this.allowShift) { this.index = dir === 1 ? this.index + 1 : this.index - 1; this.moveSlides(true); } if (stopAutoPlay && this.autoPlay) { this.handleAutoPlay(); } this.allowShift = false; } /** * Transition for the slides. * @param {Boolean} transition */ moveSlides(transition) { const newOffset = this.slides[this.index].offsetTop; const newHeight = this.slides[this.index].offsetHeight; this.content.style.height = `${newHeight}px`; this.slidesContainer.style.transitionDuration = transition ? '0.4s' : '1ms'; this.slidesContainer.style.transform = `translate3d(0px, -${newOffset}px, 0px)`; } /** * Action to update slides index and position. */ checkIndex() { // Update index if (this.index === 0) { this.index = this.total; this.moveSlides(false); } if (this.index === this.total + 1) { this.index = 1; this.moveSlides(false); } // Update pagination const currentSlide = queryOne(this.currentSlideClass, this.element); currentSlide.textContent = this.index; // Update slides if (this.slides) { this.slides.forEach((slide, index) => { const cta = queryOne('.ecl-link', slide); if (this.index === index) { slide.removeAttribute('inert', 'true'); if (cta) { cta.removeAttribute('tabindex', -1); } } else { slide.setAttribute('inert', 'true'); if (cta) { cta.setAttribute('tabindex', -1); } } }); } this.allowShift = true; } /** * Toggles play/pause slides. */ handleAutoPlay() { if (!this.autoPlay) { this.autoPlayInterval = setInterval(() => { this.shiftSlide(1); }, 5000); this.autoPlay = true; const isFocus = document.activeElement === this.btnPlay; this.btnPlay.style.display = 'none'; this.btnPause.style.display = 'flex'; if (isFocus) { this.btnPause.focus(); } } else { clearInterval(this.autoPlayInterval); this.autoPlay = false; const isFocus = document.activeElement === this.btnPause; this.btnPlay.style.display = 'flex'; this.btnPause.style.display = 'none'; if (isFocus) { this.btnPlay.focus(); } } } /** * Trigger events on mouseover. */ handleMouseOver() { this.hoverAutoPlay = this.autoPlay; if (this.hoverAutoPlay) { this.handleAutoPlay(); } return this; } /** * Trigger events on mouseout. */ handleMouseOut() { if (this.hoverAutoPlay) { this.handleAutoPlay(); } return this; } /** * Trigger events on resize. */ handleResize() { this.moveSlides(false); } /** * Trigger events on focus. * @param {Event} e */ handleFocus(e) { const focusElement = e.target; // Disable autoplay if focus is on a slide CTA if ( focusElement && focusElement.contains(document.activeElement) && this.autoPlay ) { this.handleAutoPlay(); } return this; }}export default NewsTicker;