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.maxIterations Used to limit the number of iterations when looking for css values
  10. * @param {String} options.breakpoint Breakpoint from which the script starts operating
  11. * @param {Boolean} options.attachResizeListener Whether to attach a listener on resize
  12. */
  13. export class Banner {
  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 {Banner} An instance of Banner.
  21. */
  22. static autoInit(root, { BANNER: defaultOptions = {} } = {}) {
  23. const banner = new Banner(root, defaultOptions);
  24. banner.init();
  25. root.ECLBanner = banner;
  26. return banner;
  27. }
  28. /**
  29. * An array of supported events for this component.
  30. *
  31. * @type {Array<string>}
  32. * @event Banner#onCtaClick
  33. * @memberof Banner
  34. */
  35. supportedEvents = ['onCtaClick'];
  36. constructor(
  37. element,
  38. {
  39. bannerContainer = '[data-ecl-banner-container]',
  40. bannerVPadding = '8',
  41. bannerPicture = '[data-ecl-banner-image]',
  42. breakpoint = '996',
  43. attachResizeListener = true,
  44. maxIterations = 10,
  45. } = {},
  46. ) {
  47. // Check element
  48. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  49. throw new TypeError(
  50. 'DOM element should be given to initialize this widget.',
  51. );
  52. }
  53. this.element = element;
  54. this.eventManager = new EventManager();
  55. this.bannerVPadding = bannerVPadding;
  56. this.resizeTimer = null;
  57. this.bannerContainer = queryOne(bannerContainer, this.element);
  58. this.bannerPicture = queryOne(bannerPicture, this.element);
  59. this.bannerImage = this.bannerPicture
  60. ? queryOne('img', this.bannerPicture)
  61. : false;
  62. this.bannerCTA = this.bannerPicture
  63. ? queryOne('.ecl-banner__cta', this.element)
  64. : false;
  65. this.breakpoint = breakpoint;
  66. this.attachResizeListener = attachResizeListener;
  67. this.maxIterations = maxIterations;
  68. // Bind `this` for use in callbacks
  69. this.setBannerHeight = this.setBannerHeight.bind(this);
  70. this.checkViewport = this.checkViewport.bind(this);
  71. this.resetBannerHeight = this.resetBannerHeight.bind(this);
  72. this.handleResize = this.handleResize.bind(this);
  73. this.waitForAspectRatioToBeDefined =
  74. this.waitForAspectRatioToBeDefined.bind(this);
  75. this.setHeight = this.setHeight.bind(this);
  76. }
  77. /**
  78. * Initialise component.
  79. */
  80. init() {
  81. if (!ECL) {
  82. throw new TypeError('Called init but ECL is not present');
  83. }
  84. ECL.components = ECL.components || new Map();
  85. this.defaultRatio = () => {
  86. if (this.element.classList.contains('ecl-banner--xs')) {
  87. return '6/1';
  88. }
  89. if (this.element.classList.contains('ecl-banner--s')) {
  90. return '5/1';
  91. }
  92. if (this.element.classList.contains('ecl-banner--l')) {
  93. return '3/1';
  94. }
  95. return '4/1';
  96. };
  97. if (this.attachResizeListener) {
  98. window.addEventListener('resize', this.handleResize);
  99. }
  100. if (this.bannerCTA) {
  101. this.bannerCTA.addEventListener('click', (e) => this.handleCtaClick(e));
  102. }
  103. this.checkViewport();
  104. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  105. ECL.components.set(this.element, this);
  106. }
  107. /**
  108. * Register a callback function for a specific event.
  109. *
  110. * @param {string} eventName - The name of the event to listen for.
  111. * @param {Function} callback - The callback function to be invoked when the event occurs.
  112. * @returns {void}
  113. * @memberof Banner
  114. * @instance
  115. *
  116. * @example
  117. * // Registering a callback for the 'onCtaClick' event
  118. * banner.on('onCtaClick', (event) => {
  119. * console.log('The cta was clicked', event);
  120. * });
  121. */
  122. on(eventName, callback) {
  123. this.eventManager.on(eventName, callback);
  124. }
  125. /**
  126. * Trigger a component event.
  127. *
  128. * @param {string} eventName - The name of the event to trigger.
  129. * @param {any} eventData - Data associated with the event.
  130. *
  131. * @memberof Banner
  132. */
  133. trigger(eventName, eventData) {
  134. this.eventManager.trigger(eventName, eventData);
  135. }
  136. /**
  137. * Retrieve the value of the aspect ratio in the styles.
  138. */
  139. waitForAspectRatioToBeDefined() {
  140. this.attemptCounter = (this.attemptCounter || 0) + 1;
  141. const aspectRatio = getComputedStyle(this.bannerImage).getPropertyValue(
  142. '--css-aspect-ratio',
  143. );
  144. if (
  145. (typeof aspectRatio === 'undefined' || aspectRatio === '') &&
  146. this.maxIterations > this.attemptCounter
  147. ) {
  148. setTimeout(() => this.waitForAspectRatioToBeDefined(), 100);
  149. } else {
  150. this.setHeight(aspectRatio);
  151. }
  152. }
  153. /**
  154. * Sets or resets the banner height
  155. *
  156. * @param {string} aspect ratio
  157. */
  158. setHeight(ratio) {
  159. const bannerHeight =
  160. this.bannerContainer.offsetHeight + 2 * parseInt(this.bannerVPadding, 10);
  161. const bannerWidth = parseInt(
  162. getComputedStyle(this.element).getPropertyValue('width'),
  163. 10,
  164. );
  165. const [denominator, numerator] = ratio.split('/').map(Number);
  166. const currentHeight = (bannerWidth * numerator) / denominator;
  167. if (bannerHeight > currentHeight) {
  168. if (this.bannerImage) {
  169. this.bannerImage.style.aspectRatio = 'auto';
  170. }
  171. this.element.style.height = `${bannerHeight}px`;
  172. } else {
  173. this.resetBannerHeight();
  174. }
  175. }
  176. /**
  177. * Prepare to set the banner height
  178. */
  179. setBannerHeight() {
  180. if (this.bannerImage) {
  181. this.waitForAspectRatioToBeDefined();
  182. } else {
  183. this.setHeight(this.defaultRatio());
  184. }
  185. }
  186. /**
  187. * Remove any override and get back the css
  188. */
  189. resetBannerHeight() {
  190. if (this.bannerImage) {
  191. const computedStyle = getComputedStyle(this.bannerImage);
  192. this.bannerImage.style.aspectRatio =
  193. computedStyle.getPropertyValue('--css-aspect-ratio');
  194. }
  195. this.element.style.height = 'auto';
  196. }
  197. /**
  198. * Check the current viewport width and act accordingly.
  199. */
  200. checkViewport() {
  201. if (window.innerWidth > this.breakpoint) {
  202. this.setBannerHeight();
  203. } else {
  204. this.resetBannerHeight();
  205. }
  206. }
  207. /**
  208. * Trigger events on resize
  209. * Uses a debounce, for performance
  210. */
  211. handleResize() {
  212. clearTimeout(this.resizeTimer);
  213. this.resizeTimer = setTimeout(() => {
  214. this.checkViewport();
  215. }, 200);
  216. }
  217. /**
  218. * Triggers a custom event when clicking on the cta.
  219. *
  220. * @param {e} Event
  221. * @fires Banner#onCtaClick
  222. */
  223. handleCtaClick(e) {
  224. let href = null;
  225. const anchor = e.target.closest('a');
  226. if (anchor) {
  227. href = anchor.getAttribute('href');
  228. }
  229. const eventData = { item: this.bannerCTA, target: href || e.target };
  230. this.trigger('onCtaClick', eventData);
  231. }
  232. /**
  233. * Destroy component.
  234. */
  235. destroy() {
  236. this.resetBannerHeight();
  237. this.element.removeAttribute('data-ecl-auto-initialized');
  238. ECL.components.delete(this.element);
  239. if (this.attachResizeListener) {
  240. window.removeEventListener('resize', this.handleResize);
  241. }
  242. if (this.bannerCTA) {
  243. this.bannerCTA.removeEventListener('click', this.handleCtaClick);
  244. }
  245. }
  246. }
  247. export default Banner;