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. this.resizeTimer = null;
  83. // Bind `this` for use in callbacks
  84. this.handleAutoPlay = this.handleAutoPlay.bind(this);
  85. this.handleMouseOver = this.handleMouseOver.bind(this);
  86. this.handleMouseOut = this.handleMouseOut.bind(this);
  87. this.shiftSlide = this.shiftSlide.bind(this);
  88. this.checkIndex = this.checkIndex.bind(this);
  89. this.moveSlides = this.moveSlides.bind(this);
  90. this.handleResize = this.handleResize.bind(this);
  91. this.handleFocus = this.handleFocus.bind(this);
  92. }
  93. /**
  94. * Initialise component.
  95. */
  96. init() {
  97. if (!ECL) {
  98. throw new TypeError('Called init but ECL is not present');
  99. }
  100. ECL.components = ECL.components || new Map();
  101. this.btnPlay = queryOne(this.playSelector, this.element);
  102. this.btnPause = queryOne(this.pauseSelector, this.element);
  103. this.btnPrev = queryOne(this.prevSelector, this.element);
  104. this.btnNext = queryOne(this.nextSelector, this.element);
  105. this.slidesContainer = queryOne(this.slidesClass, this.element);
  106. this.container = queryOne(this.containerClass, this.element);
  107. this.content = queryOne(this.contentClass, this.element);
  108. this.controls = queryOne(this.controlsClass, this.element);
  109. this.slides = queryAll(this.slideClass, this.element);
  110. this.total = this.slides.length;
  111. // If only one slide, don't initialize ticker and hide controls
  112. if (this.total <= 1 && this.controls) {
  113. this.content.style.height = 'auto';
  114. this.controls.style.display = 'none';
  115. return false;
  116. }
  117. const firstSlide = this.slides[0];
  118. const lastSlide = this.slides[this.slides.length - 1];
  119. this.cloneFirstSLide = firstSlide.cloneNode(true);
  120. this.cloneLastSLide = lastSlide.cloneNode(true);
  121. // Clone first and last slide
  122. this.slidesContainer.appendChild(this.cloneFirstSLide);
  123. this.slidesContainer.insertBefore(this.cloneLastSLide, firstSlide);
  124. // Refresh the slides variable after adding new cloned slides
  125. this.slides = queryAll(this.slideClass, this.element);
  126. // Initialize ticker position and size
  127. this.handleResize();
  128. // Activate autoPlay
  129. this.handleAutoPlay();
  130. // Bind events
  131. if (this.attachClickListener && this.btnPlay && this.btnPause) {
  132. this.btnPlay.addEventListener('click', this.handleAutoPlay);
  133. this.btnPause.addEventListener('click', this.handleAutoPlay);
  134. }
  135. if (this.attachClickListener && this.btnNext) {
  136. this.btnNext.addEventListener(
  137. 'click',
  138. this.shiftSlide.bind(this, 1, true),
  139. );
  140. }
  141. if (this.attachClickListener && this.btnPrev) {
  142. this.btnPrev.addEventListener(
  143. 'click',
  144. this.shiftSlide.bind(this, -1, true),
  145. );
  146. }
  147. if (this.slidesContainer) {
  148. this.slidesContainer.addEventListener('transitionend', this.checkIndex);
  149. this.slidesContainer.addEventListener('mouseover', this.handleMouseOver);
  150. this.slidesContainer.addEventListener('mouseout', this.handleMouseOut);
  151. }
  152. if (this.container) {
  153. this.container.addEventListener('focus', this.handleFocus, true);
  154. }
  155. if (this.attachResizeListener) {
  156. window.addEventListener('resize', this.handleResize);
  157. }
  158. // Set ecl initialized attribute
  159. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  160. ECL.components.set(this.element, this);
  161. return this;
  162. }
  163. /**
  164. * Destroy component.
  165. */
  166. destroy() {
  167. if (this.cloneFirstSLide && this.cloneLastSLide) {
  168. this.cloneFirstSLide.remove();
  169. this.cloneLastSLide.remove();
  170. }
  171. if (this.btnPlay) {
  172. this.btnPlay.replaceWith(this.btnPlay.cloneNode(true));
  173. }
  174. if (this.btnPause) {
  175. this.btnPause.replaceWith(this.btnPause.cloneNode(true));
  176. }
  177. if (this.btnNext) {
  178. this.btnNext.replaceWith(this.btnNext.cloneNode(true));
  179. }
  180. if (this.btnPrev) {
  181. this.btnPrev.replaceWith(this.btnPrev.cloneNode(true));
  182. }
  183. if (this.slidesContainer) {
  184. this.slidesContainer.removeEventListener(
  185. 'transitionend',
  186. this.checkIndex,
  187. );
  188. this.slidesContainer.removeEventListener(
  189. 'mouseover',
  190. this.handleMouseOver,
  191. );
  192. this.slidesContainer.removeEventListener('mouseout', this.handleMouseOut);
  193. }
  194. if (this.container) {
  195. this.container.removeEventListener('focus', this.handleFocus, true);
  196. }
  197. if (this.attachResizeListener) {
  198. window.removeEventListener('resize', this.handleResize);
  199. }
  200. if (this.autoPlayInterval) {
  201. clearInterval(this.autoPlayInterval);
  202. this.autoPlay = null;
  203. }
  204. if (this.element) {
  205. this.element.removeAttribute('data-ecl-auto-initialized');
  206. ECL.components.delete(this.element);
  207. }
  208. }
  209. /**
  210. * Action to shift next or previous slide.
  211. * @param {int} dir
  212. * @param {Boolean} stopAutoPlay
  213. */
  214. shiftSlide(dir, stopAutoPlay) {
  215. if (this.allowShift) {
  216. this.index = dir === 1 ? this.index + 1 : this.index - 1;
  217. this.moveSlides(true);
  218. }
  219. if (stopAutoPlay && this.autoPlay) {
  220. this.handleAutoPlay();
  221. }
  222. this.allowShift = false;
  223. }
  224. /**
  225. * Transition for the slides.
  226. * @param {Boolean} transition
  227. */
  228. moveSlides(transition) {
  229. const newOffset = this.slides[this.index].offsetTop;
  230. const newHeight = this.slides[this.index].offsetHeight;
  231. this.content.style.height = `${newHeight}px`;
  232. this.slidesContainer.style.transitionDuration = transition ? '0.4s' : '1ms';
  233. this.slidesContainer.style.transform = `translate3d(0px, -${newOffset}px, 0px)`;
  234. }
  235. /**
  236. * Action to update slides index and position.
  237. */
  238. checkIndex() {
  239. // Update index
  240. if (this.index === 0) {
  241. this.index = this.total;
  242. this.moveSlides(false);
  243. }
  244. if (this.index === this.total + 1) {
  245. this.index = 1;
  246. this.moveSlides(false);
  247. }
  248. // Update pagination
  249. const currentSlide = queryOne(this.currentSlideClass, this.element);
  250. currentSlide.textContent = this.index;
  251. // Update slides
  252. if (this.slides) {
  253. this.slides.forEach((slide, index) => {
  254. const cta = queryOne('.ecl-link', slide);
  255. if (this.index === index) {
  256. slide.removeAttribute('inert', 'true');
  257. if (cta) {
  258. cta.removeAttribute('tabindex', -1);
  259. }
  260. } else {
  261. slide.setAttribute('inert', 'true');
  262. if (cta) {
  263. cta.setAttribute('tabindex', -1);
  264. }
  265. }
  266. });
  267. }
  268. this.allowShift = true;
  269. }
  270. /**
  271. * Toggles play/pause slides.
  272. */
  273. handleAutoPlay() {
  274. if (!this.autoPlay) {
  275. this.autoPlayInterval = setInterval(() => {
  276. this.shiftSlide(1);
  277. }, 5000);
  278. this.autoPlay = true;
  279. const isFocus = document.activeElement === this.btnPlay;
  280. this.btnPlay.style.display = 'none';
  281. this.btnPause.style.display = 'flex';
  282. if (isFocus) {
  283. this.btnPause.focus();
  284. }
  285. } else {
  286. clearInterval(this.autoPlayInterval);
  287. this.autoPlay = false;
  288. const isFocus = document.activeElement === this.btnPause;
  289. this.btnPlay.style.display = 'flex';
  290. this.btnPause.style.display = 'none';
  291. if (isFocus) {
  292. this.btnPlay.focus();
  293. }
  294. }
  295. }
  296. /**
  297. * Trigger events on mouseover.
  298. */
  299. handleMouseOver() {
  300. this.hoverAutoPlay = this.autoPlay;
  301. if (this.hoverAutoPlay) {
  302. this.handleAutoPlay();
  303. }
  304. return this;
  305. }
  306. /**
  307. * Trigger events on mouseout.
  308. */
  309. handleMouseOut() {
  310. if (this.hoverAutoPlay) {
  311. this.handleAutoPlay();
  312. }
  313. return this;
  314. }
  315. /**
  316. * Trigger events on resize.
  317. */
  318. handleResize() {
  319. clearTimeout(this.resizeTimer);
  320. this.resizeTimer = setTimeout(() => {
  321. this.moveSlides(false);
  322. }, 100);
  323. }
  324. /**
  325. * Trigger events on focus.
  326. * @param {Event} e
  327. */
  328. handleFocus(e) {
  329. const focusElement = e.target;
  330. // Disable autoplay if focus is on a slide CTA
  331. if (
  332. focusElement &&
  333. focusElement.contains(document.activeElement) &&
  334. this.autoPlay
  335. ) {
  336. this.handleAutoPlay();
  337. }
  338. return this;
  339. }
  340. }
  341. export default NewsTicker;