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