banner.js

  1. import { queryOne } from '@ecl/dom-utils';
  2. import EventManager from '@ecl/event-manager';
  3. /**
  4. * @param {HTMLElement} element DOM element for component instantiation and scope
  5. * @param {Object} options
  6. * @param {String} options.bannerContainer Selector for the banner content
  7. * @param {String} options.bannerVPadding Optional additional padding
  8. * @param {String} options.bannerPicture Selector for the banner picture
  9. * @param {String} options.bannerVideo Selector for the banner video
  10. * @param {String} options.bannerPlay Selector for the banner play button
  11. * @param {String} options.bannerPause Selector for the banner pause button
  12. * @param {String} options.maxIterations Used to limit the number of iterations when looking for css values
  13. * @param {String} options.breakpoint Breakpoint from which the script starts operating
  14. * @param {Boolean} options.attachResizeListener Whether to attach a listener on resize
  15. */
  16. export class Banner {
  17. /**
  18. * @static
  19. * Shorthand for instance creation and initialisation.
  20. *
  21. * @param {HTMLElement} root DOM element for component instantiation and scope
  22. *
  23. * @return {Banner} An instance of Banner.
  24. */
  25. static autoInit(root, { BANNER: defaultOptions = {} } = {}) {
  26. const banner = new Banner(root, defaultOptions);
  27. banner.init();
  28. root.ECLBanner = banner;
  29. return banner;
  30. }
  31. /**
  32. * An array of supported events for this component.
  33. *
  34. * @type {Array<string>}
  35. * @event Banner#onCtaClick
  36. * @event Banner#onPlayClick
  37. * @event Banner#onPauseClick
  38. * @memberof Banner
  39. */
  40. supportedEvents = ['onCtaClick', 'onPlayClick', 'onPauseClick'];
  41. constructor(
  42. element,
  43. {
  44. bannerContainer = '[data-ecl-banner-container]',
  45. bannerVPadding = '8',
  46. bannerPicture = '[data-ecl-banner-image]',
  47. bannerVideo = '[data-ecl-banner-video]',
  48. bannerPlay = '[data-ecl-banner-play]',
  49. bannerPause = '[data-ecl-banner-pause]',
  50. breakpoint = '996',
  51. attachResizeListener = true,
  52. maxIterations = 10,
  53. } = {},
  54. ) {
  55. // Check element
  56. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  57. throw new TypeError(
  58. 'DOM element should be given to initialize this widget.',
  59. );
  60. }
  61. this.element = element;
  62. this.eventManager = new EventManager();
  63. this.bannerVPadding = bannerVPadding;
  64. this.resizeTimer = null;
  65. this.bannerContainer = queryOne(bannerContainer, this.element);
  66. this.bannerPicture = queryOne(bannerPicture, this.element);
  67. this.bannerVideo = queryOne(bannerVideo, this.element);
  68. this.bannerPlay = queryOne(bannerPlay, this.element);
  69. this.bannerPause = queryOne(bannerPause, this.element);
  70. this.bannerImage = this.bannerPicture
  71. ? queryOne('img', this.bannerPicture)
  72. : false;
  73. this.bannerCTA = this.bannerPicture
  74. ? queryOne('.ecl-banner__cta', this.element)
  75. : false;
  76. this.breakpoint = breakpoint;
  77. this.attachResizeListener = attachResizeListener;
  78. this.maxIterations = maxIterations;
  79. // Bind `this` for use in callbacks
  80. this.setBannerHeight = this.setBannerHeight.bind(this);
  81. this.checkViewport = this.checkViewport.bind(this);
  82. this.resetBannerHeight = this.resetBannerHeight.bind(this);
  83. this.handleResize = this.handleResize.bind(this);
  84. this.waitForAspectRatioToBeDefined =
  85. this.waitForAspectRatioToBeDefined.bind(this);
  86. this.setHeight = this.setHeight.bind(this);
  87. }
  88. /**
  89. * Initialise component.
  90. */
  91. init() {
  92. if (!ECL) {
  93. throw new TypeError('Called init but ECL is not present');
  94. }
  95. ECL.components = ECL.components || new Map();
  96. this.defaultRatio = () => {
  97. if (this.element.classList.contains('ecl-banner--xs')) {
  98. return '6/1';
  99. }
  100. if (this.element.classList.contains('ecl-banner--s')) {
  101. return '5/1';
  102. }
  103. if (this.element.classList.contains('ecl-banner--l')) {
  104. return '3/1';
  105. }
  106. return '4/1';
  107. };
  108. if (this.attachResizeListener) {
  109. window.addEventListener('resize', this.handleResize);
  110. }
  111. if (this.bannerCTA) {
  112. this.bannerCTA.addEventListener('click', (e) => this.handleCtaClick(e));
  113. }
  114. if (this.bannerPlay) {
  115. this.bannerPlay.addEventListener('click', (e) => this.handlePlayClick(e));
  116. this.bannerPlay.style.display = 'none';
  117. }
  118. if (this.bannerPause) {
  119. this.bannerPause.addEventListener('click', (e) =>
  120. this.handlePauseClick(e),
  121. );
  122. this.bannerPause.style.display = 'flex';
  123. }
  124. this.checkViewport();
  125. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  126. ECL.components.set(this.element, this);
  127. }
  128. /**
  129. * Register a callback function for a specific event.
  130. *
  131. * @param {string} eventName - The name of the event to listen for.
  132. * @param {Function} callback - The callback function to be invoked when the event occurs.
  133. * @returns {void}
  134. * @memberof Banner
  135. * @instance
  136. *
  137. * @example
  138. * // Registering a callback for the 'onCtaClick' event
  139. * banner.on('onCtaClick', (event) => {
  140. * console.log('The cta was clicked', event);
  141. * });
  142. */
  143. on(eventName, callback) {
  144. this.eventManager.on(eventName, callback);
  145. }
  146. /**
  147. * Trigger a component event.
  148. *
  149. * @param {string} eventName - The name of the event to trigger.
  150. * @param {any} eventData - Data associated with the event.
  151. *
  152. * @memberof Banner
  153. */
  154. trigger(eventName, eventData) {
  155. this.eventManager.trigger(eventName, eventData);
  156. }
  157. /**
  158. * Retrieve the value of the aspect ratio in the styles.
  159. */
  160. waitForAspectRatioToBeDefined() {
  161. this.attemptCounter = (this.attemptCounter || 0) + 1;
  162. let aspectRatio = '';
  163. if (this.bannerVideo) {
  164. // Ensure that the video is loaded (width > 0) before passing the ratio
  165. if (this.bannerVideo.videoWidth > 0) {
  166. aspectRatio = this.defaultRatio();
  167. }
  168. } else if (this.bannerImage) {
  169. aspectRatio = getComputedStyle(this.bannerImage).getPropertyValue(
  170. '--css-aspect-ratio',
  171. );
  172. }
  173. if (
  174. (typeof aspectRatio === 'undefined' || aspectRatio === '') &&
  175. this.maxIterations > this.attemptCounter
  176. ) {
  177. setTimeout(() => this.waitForAspectRatioToBeDefined(), 100);
  178. } else {
  179. this.setHeight(aspectRatio);
  180. }
  181. }
  182. /**
  183. * Sets or resets the banner height
  184. *
  185. * @param {string} aspect ratio
  186. */
  187. setHeight(ratio) {
  188. const bannerHeight =
  189. this.bannerContainer.offsetHeight + 2 * parseInt(this.bannerVPadding, 10);
  190. const bannerWidth = parseInt(
  191. getComputedStyle(this.element).getPropertyValue('width'),
  192. 10,
  193. );
  194. const [denominator, numerator] = ratio.split('/').map(Number);
  195. const currentHeight = (bannerWidth * numerator) / denominator;
  196. if (bannerHeight > currentHeight) {
  197. if (this.bannerImage) {
  198. this.bannerImage.style.aspectRatio = 'auto';
  199. }
  200. if (this.bannerVideo) {
  201. this.bannerVideo.style.aspectRatio = 'auto';
  202. }
  203. this.element.style.height = `${bannerHeight}px`;
  204. } else {
  205. this.resetBannerHeight();
  206. }
  207. }
  208. /**
  209. * Prepare to set the banner height
  210. */
  211. setBannerHeight() {
  212. if (this.bannerImage || this.bannerVideo) {
  213. this.waitForAspectRatioToBeDefined();
  214. } else {
  215. this.setHeight(this.defaultRatio());
  216. }
  217. }
  218. /**
  219. * Remove any override and get back the css
  220. */
  221. resetBannerHeight() {
  222. if (this.bannerImage) {
  223. const computedStyle = getComputedStyle(this.bannerImage);
  224. this.bannerImage.style.aspectRatio =
  225. computedStyle.getPropertyValue('--css-aspect-ratio');
  226. }
  227. if (this.bannerVideo) {
  228. this.bannerVideo.style.aspectRatio = this.defaultRatio();
  229. }
  230. this.element.style.height = 'auto';
  231. }
  232. /**
  233. * Check the current viewport width and act accordingly.
  234. */
  235. checkViewport() {
  236. if (window.innerWidth > this.breakpoint) {
  237. this.setBannerHeight();
  238. } else {
  239. this.resetBannerHeight();
  240. }
  241. }
  242. /**
  243. * Trigger events on resize
  244. * Uses a debounce, for performance
  245. */
  246. handleResize() {
  247. clearTimeout(this.resizeTimer);
  248. this.resizeTimer = setTimeout(() => {
  249. this.checkViewport();
  250. }, 200);
  251. }
  252. /**
  253. * Triggers a custom event when clicking on the cta.
  254. *
  255. * @param {e} Event
  256. * @fires Banner#onCtaClick
  257. */
  258. handleCtaClick(e) {
  259. let href = null;
  260. const anchor = e.target.closest('a');
  261. if (anchor) {
  262. href = anchor.getAttribute('href');
  263. }
  264. const eventData = { item: this.bannerCTA, target: href || e.target };
  265. this.trigger('onCtaClick', eventData);
  266. }
  267. /**
  268. * Triggers a custom event when clicking on the play button.
  269. *
  270. * @param {e} Event
  271. * @fires Banner#onPlayClick
  272. */
  273. handlePlayClick() {
  274. if (this.bannerVideo) {
  275. this.bannerVideo.play();
  276. }
  277. this.bannerPlay.style.display = 'none';
  278. if (this.bannerPause) {
  279. this.bannerPause.style.display = 'flex';
  280. this.bannerPause.focus();
  281. }
  282. const eventData = { item: this.bannerPlay };
  283. this.trigger('onPlayClick', eventData);
  284. }
  285. /**
  286. * Triggers a custom event when clicking on the pause button.
  287. *
  288. * @param {e} Event
  289. * @fires Banner#onPauseClick
  290. */
  291. handlePauseClick() {
  292. if (this.bannerVideo) {
  293. this.bannerVideo.pause();
  294. }
  295. this.bannerPause.style.display = 'none';
  296. if (this.bannerPlay) {
  297. this.bannerPlay.style.display = 'flex';
  298. this.bannerPlay.focus();
  299. }
  300. const eventData = { item: this.bannerPause };
  301. this.trigger('onPauseClick', eventData);
  302. }
  303. /**
  304. * Destroy component.
  305. */
  306. destroy() {
  307. this.resetBannerHeight();
  308. this.element.removeAttribute('data-ecl-auto-initialized');
  309. ECL.components.delete(this.element);
  310. if (this.attachResizeListener) {
  311. window.removeEventListener('resize', this.handleResize);
  312. }
  313. if (this.bannerCTA) {
  314. this.bannerCTA.removeEventListener('click', this.handleCtaClick);
  315. }
  316. if (this.bannerPlay) {
  317. this.bannerPlay.removeEventListener('click', this.handlePlayClick);
  318. }
  319. if (this.bannerPause) {
  320. this.bannerPause.removeEventListener('click', this.handlePauseClick);
  321. }
  322. }
  323. }
  324. export default Banner;