news-ticker.js

  1. import { queryOne, queryAll } from '@ecl/dom-utils';
  2. /**
  3. * @param {HTMLElement} element DOM element for component instantiation and scope
  4. * @param {Object} options
  5. * @param {String} options.toggleSelector Selector for toggling element
  6. * @param {String} options.prevSelector Selector for prev element
  7. * @param {String} options.nextSelector Selector for next element
  8. * @param {String} options.contentClass Selector for the content container
  9. * @param {String} options.slidesClass Selector for the slides container
  10. * @param {String} options.slideClass Selector for the slide items
  11. * @param {String} options.currentSlideClass Selector for the counter current slide number
  12. */
  13. export class NewsTicker {
  14. /**
  15. * @static
  16. * Shorthand for instance creation and initialisation.
  17. *
  18. * @param {HTMLElement} root DOM element for component instantiation and scope
  19. *
  20. * @return {NewsTicker} An instance of News ticker.
  21. */
  22. static autoInit(root, { NEWS_TICKER: defaultOptions = {} } = {}) {
  23. const newsTicker = new NewsTicker(root, defaultOptions);
  24. newsTicker.init();
  25. root.ECLNewsTicker = newsTicker;
  26. return newsTicker;
  27. }
  28. constructor(
  29. element,
  30. {
  31. playSelector = '[data-ecl-news-ticker-play]',
  32. pauseSelector = '[data-ecl-news-ticker-pause]',
  33. prevSelector = '[data-ecl-news-ticker-prev]',
  34. nextSelector = '[data-ecl-news-ticker-next]',
  35. containerClass = '.ecl-news-ticker__container',
  36. contentClass = '.ecl-news-ticker__content',
  37. slidesClass = '.ecl-news-ticker__slides',
  38. slideClass = '.ecl-news-ticker__slide',
  39. currentSlideClass = '.ecl-news-ticker__counter--current',
  40. controlsClass = '.ecl-news-ticker__controls',
  41. attachClickListener = true,
  42. attachResizeListener = true,
  43. } = {},
  44. ) {
  45. // Check element
  46. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  47. throw new TypeError(
  48. 'DOM element should be given to initialize this widget.',
  49. );
  50. }
  51. this.element = element;
  52. // Options
  53. this.playSelector = playSelector;
  54. this.pauseSelector = pauseSelector;
  55. this.prevSelector = prevSelector;
  56. this.nextSelector = nextSelector;
  57. this.containerClass = containerClass;
  58. this.contentClass = contentClass;
  59. this.slidesClass = slidesClass;
  60. this.slideClass = slideClass;
  61. this.currentSlideClass = currentSlideClass;
  62. this.controlsClass = controlsClass;
  63. this.attachClickListener = attachClickListener;
  64. this.attachResizeListener = attachResizeListener;
  65. // Private variables
  66. this.container = null;
  67. this.content = null;
  68. this.slides = null;
  69. this.btnPlay = null;
  70. this.btnPause = null;
  71. this.btnPrev = null;
  72. this.btnNext = null;
  73. this.index = 1;
  74. this.total = 0;
  75. this.allowShift = true;
  76. this.autoPlay = null;
  77. this.autoPlayInterval = null;
  78. this.hoverAutoPlay = null;
  79. this.resizeTimer = null;
  80. this.cloneFirstSLide = null;
  81. this.cloneLastSLide = null;
  82. // Bind `this` for use in callbacks
  83. this.handleAutoPlay = this.handleAutoPlay.bind(this);
  84. this.handleMouseOver = this.handleMouseOver.bind(this);
  85. this.handleMouseOut = this.handleMouseOut.bind(this);
  86. this.shiftSlide = this.shiftSlide.bind(this);
  87. this.checkIndex = this.checkIndex.bind(this);
  88. this.moveSlides = this.moveSlides.bind(this);
  89. this.handleResize = this.handleResize.bind(this);
  90. this.handleFocus = this.handleFocus.bind(this);
  91. }
  92. /**
  93. * Initialise component.
  94. */
  95. init() {
  96. if (!ECL) {
  97. throw new TypeError('Called init but ECL is not present');
  98. }
  99. ECL.components = ECL.components || new Map();
  100. this.btnPlay = queryOne(this.playSelector, this.element);
  101. this.btnPause = queryOne(this.pauseSelector, this.element);
  102. this.btnPrev = queryOne(this.prevSelector, this.element);
  103. this.btnNext = queryOne(this.nextSelector, this.element);
  104. this.slidesContainer = queryOne(this.slidesClass, this.element);
  105. this.container = queryOne(this.containerClass, this.element);
  106. this.content = queryOne(this.contentClass, this.element);
  107. this.controls = queryOne(this.controlsClass, this.element);
  108. this.slides = queryAll(this.slideClass, this.element);
  109. this.total = this.slides.length;
  110. // If only one slide, don't initialize ticker and hide controls
  111. if (this.total <= 1 && this.controls) {
  112. this.content.style.height = 'auto';
  113. this.controls.style.display = 'none';
  114. return false;
  115. }
  116. const firstSlide = this.slides[0];
  117. const lastSlide = this.slides[this.slides.length - 1];
  118. this.cloneFirstSLide = firstSlide.cloneNode(true);
  119. this.cloneLastSLide = lastSlide.cloneNode(true);
  120. // Clone first and last slide
  121. this.slidesContainer.appendChild(this.cloneFirstSLide);
  122. this.slidesContainer.insertBefore(this.cloneLastSLide, firstSlide);
  123. // Refresh the slides variable after adding new cloned slides
  124. this.slides = queryAll(this.slideClass, this.element);
  125. // Initialize ticker position and size
  126. this.handleResize();
  127. // Activate autoPlay
  128. this.handleAutoPlay();
  129. // Bind events
  130. if (this.attachClickListener && this.btnPlay && this.btnPause) {
  131. this.btnPlay.addEventListener('click', this.handleAutoPlay);
  132. this.btnPause.addEventListener('click', this.handleAutoPlay);
  133. }
  134. if (this.attachClickListener && this.btnNext) {
  135. this.btnNext.addEventListener(
  136. 'click',
  137. this.shiftSlide.bind(this, 1, true),
  138. );
  139. }
  140. if (this.attachClickListener && this.btnPrev) {
  141. this.btnPrev.addEventListener(
  142. 'click',
  143. this.shiftSlide.bind(this, -1, true),
  144. );
  145. }
  146. if (this.slidesContainer) {
  147. this.slidesContainer.addEventListener('transitionend', this.checkIndex);
  148. this.slidesContainer.addEventListener('mouseover', this.handleMouseOver);
  149. this.slidesContainer.addEventListener('mouseout', this.handleMouseOut);
  150. }
  151. if (this.container) {
  152. this.container.addEventListener('focus', this.handleFocus, true);
  153. }
  154. if (this.attachResizeListener) {
  155. window.addEventListener('resize', this.handleResize);
  156. }
  157. // Set ecl initialized attribute
  158. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  159. ECL.components.set(this.element, this);
  160. return this;
  161. }
  162. /**
  163. * Destroy component.
  164. */
  165. destroy() {
  166. if (this.cloneFirstSLide && this.cloneLastSLide) {
  167. this.cloneFirstSLide.remove();
  168. this.cloneLastSLide.remove();
  169. }
  170. if (this.btnPlay) {
  171. this.btnPlay.replaceWith(this.btnPlay.cloneNode(true));
  172. }
  173. if (this.btnPause) {
  174. this.btnPause.replaceWith(this.btnPause.cloneNode(true));
  175. }
  176. if (this.btnNext) {
  177. this.btnNext.replaceWith(this.btnNext.cloneNode(true));
  178. }
  179. if (this.btnPrev) {
  180. this.btnPrev.replaceWith(this.btnPrev.cloneNode(true));
  181. }
  182. if (this.slidesContainer) {
  183. this.slidesContainer.removeEventListener(
  184. 'transitionend',
  185. this.checkIndex,
  186. );
  187. this.slidesContainer.removeEventListener(
  188. 'mouseover',
  189. this.handleMouseOver,
  190. );
  191. this.slidesContainer.removeEventListener('mouseout', this.handleMouseOut);
  192. }
  193. if (this.container) {
  194. this.container.removeEventListener('focus', this.handleFocus, true);
  195. }
  196. if (this.attachResizeListener) {
  197. window.removeEventListener('resize', this.handleResize);
  198. }
  199. if (this.autoPlayInterval) {
  200. clearInterval(this.autoPlayInterval);
  201. this.autoPlay = null;
  202. }
  203. if (this.element) {
  204. this.element.removeAttribute('data-ecl-auto-initialized');
  205. ECL.components.delete(this.element);
  206. }
  207. }
  208. /**
  209. * Action to shift next or previous slide.
  210. * @param {int} dir
  211. * @param {Boolean} stopAutoPlay
  212. */
  213. shiftSlide(dir, stopAutoPlay) {
  214. if (this.allowShift) {
  215. this.index = dir === 1 ? this.index + 1 : this.index - 1;
  216. this.moveSlides(true);
  217. }
  218. if (stopAutoPlay && this.autoPlay) {
  219. this.handleAutoPlay();
  220. }
  221. this.allowShift = false;
  222. }
  223. /**
  224. * Transition for the slides.
  225. * @param {Boolean} transition
  226. */
  227. moveSlides(transition) {
  228. const newOffset = this.slides[this.index].offsetTop;
  229. const newHeight = this.slides[this.index].offsetHeight;
  230. this.content.style.height = `${newHeight}px`;
  231. this.slidesContainer.style.transitionDuration = transition ? '0.4s' : '1ms';
  232. this.slidesContainer.style.transform = `translate3d(0px, -${newOffset}px, 0px)`;
  233. }
  234. /**
  235. * Action to update slides index and position.
  236. */
  237. checkIndex() {
  238. // Update index
  239. if (this.index === 0) {
  240. this.index = this.total;
  241. this.moveSlides(false);
  242. }
  243. if (this.index === this.total + 1) {
  244. this.index = 1;
  245. this.moveSlides(false);
  246. }
  247. // Update pagination
  248. const currentSlide = queryOne(this.currentSlideClass, this.element);
  249. currentSlide.textContent = this.index;
  250. // Update slides
  251. if (this.slides) {
  252. this.slides.forEach((slide, index) => {
  253. const cta = queryOne('.ecl-link', slide);
  254. if (this.index === index) {
  255. slide.removeAttribute('inert', 'true');
  256. if (cta) {
  257. cta.removeAttribute('tabindex', -1);
  258. }
  259. } else {
  260. slide.setAttribute('inert', 'true');
  261. if (cta) {
  262. cta.setAttribute('tabindex', -1);
  263. }
  264. }
  265. });
  266. }
  267. this.allowShift = true;
  268. }
  269. /**
  270. * Toggles play/pause slides.
  271. */
  272. handleAutoPlay() {
  273. if (!this.autoPlay) {
  274. this.autoPlayInterval = setInterval(() => {
  275. this.shiftSlide(1);
  276. }, 5000);
  277. this.autoPlay = true;
  278. const isFocus = document.activeElement === this.btnPlay;
  279. this.btnPlay.style.display = 'none';
  280. this.btnPause.style.display = 'flex';
  281. if (isFocus) {
  282. this.btnPause.focus();
  283. }
  284. } else {
  285. clearInterval(this.autoPlayInterval);
  286. this.autoPlay = false;
  287. const isFocus = document.activeElement === this.btnPause;
  288. this.btnPlay.style.display = 'flex';
  289. this.btnPause.style.display = 'none';
  290. if (isFocus) {
  291. this.btnPlay.focus();
  292. }
  293. }
  294. }
  295. /**
  296. * Trigger events on mouseover.
  297. */
  298. handleMouseOver() {
  299. this.hoverAutoPlay = this.autoPlay;
  300. if (this.hoverAutoPlay) {
  301. this.handleAutoPlay();
  302. }
  303. return this;
  304. }
  305. /**
  306. * Trigger events on mouseout.
  307. */
  308. handleMouseOut() {
  309. if (this.hoverAutoPlay) {
  310. this.handleAutoPlay();
  311. }
  312. return this;
  313. }
  314. /**
  315. * Trigger events on resize.
  316. */
  317. handleResize() {
  318. this.moveSlides(false);
  319. }
  320. /**
  321. * Trigger events on focus.
  322. * @param {Event} e
  323. */
  324. handleFocus(e) {
  325. const focusElement = e.target;
  326. // Disable autoplay if focus is on a slide CTA
  327. if (
  328. focusElement &&
  329. focusElement.contains(document.activeElement) &&
  330. this.autoPlay
  331. ) {
  332. this.handleAutoPlay();
  333. }
  334. return this;
  335. }
  336. }
  337. export default NewsTicker;