content-block.js

  1. import { queryOne } from '@ecl/dom-utils';
  2. /**
  3. * @param {HTMLElement} element DOM element for component instantiation and scope
  4. * @param {Object} options
  5. * @param {Boolean} options.attachClickListener Whether or not to bind click events
  6. * @param (String) options.targetSelector The selector of the element where to attach the click listener
  7. * @param (String) options.titleSelector The selector of the element containing the link
  8. * @param (Integer) options.maxIterations Maximum number of ancestors to look for the element
  9. */
  10. export class ContentBlock {
  11. /**
  12. * @static
  13. * Shorthand for instance creation and initialisation.
  14. *
  15. * @param {HTMLElement} root DOM element for component instantiation and scope
  16. *
  17. * @return {ContentBlock} An instance of ContentBlock.
  18. */
  19. static autoInit(root, { CONTENT_BLOCK: defaultOptions = {} } = {}) {
  20. const contentBlock = new ContentBlock(root, defaultOptions);
  21. contentBlock.init();
  22. root.ECLContentBlock = contentBlock;
  23. return contentBlock;
  24. }
  25. constructor(
  26. element,
  27. {
  28. targetSelector = '[data-ecl-picture-link]',
  29. titleSelector = '[data-ecl-title-link]',
  30. attachClickListener = true,
  31. maxIterations = 1,
  32. withTitleAttr = false,
  33. } = {},
  34. ) {
  35. // Check element
  36. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  37. throw new TypeError(
  38. 'DOM element should be given to initialize this widget.',
  39. );
  40. }
  41. this.element = element;
  42. // Options
  43. this.targetSelector = targetSelector;
  44. this.titleSelector = titleSelector;
  45. this.attachClickListener = attachClickListener;
  46. this.maxIterations = maxIterations;
  47. this.withTitleAttr = withTitleAttr;
  48. // Bind `this` for use in callbacks
  49. this.linkTo = this.linkTo.bind(this);
  50. this.findElementInCommonAncestor =
  51. this.findElementInCommonAncestor.bind(this);
  52. }
  53. /**
  54. * Initialise component.
  55. */
  56. init() {
  57. if (!ECL) {
  58. throw new TypeError('Called init but ECL is not present');
  59. }
  60. ECL.components = ECL.components || new Map();
  61. this.picture = this.findElementInCommonAncestor(
  62. this.element,
  63. this.targetSelector,
  64. this.maxIterations,
  65. );
  66. // Exit early if no picture is present.
  67. if (!this.picture) {
  68. return;
  69. }
  70. this.title = queryOne(this.titleSelector, this.element);
  71. this.linkEl = this.title ? queryOne('a', this.title) : false;
  72. if (this.linkEl) {
  73. this.picture.style.cursor = 'pointer';
  74. const img = queryOne('img', this.picture);
  75. if (img && this.withTitleAttr) {
  76. img.title = this.constructor.convertToFullURL(
  77. this.linkEl.getAttribute('href'),
  78. );
  79. }
  80. if (this.attachClickListener) {
  81. this.picture.addEventListener('click', this.linkTo);
  82. }
  83. }
  84. this.element.setAttribute('data-ecl-auto-initialized', true);
  85. ECL.components.set(this.element, this);
  86. }
  87. /**
  88. * Redirect the user to the desired url.
  89. */
  90. linkTo() {
  91. if (this.linkEl) {
  92. // Click the linking element.
  93. this.linkEl.click();
  94. }
  95. }
  96. /**
  97. * Find an element in a common ancestor.
  98. *
  99. * @param {HTMLElement} element
  100. * @param {string} selector
  101. */
  102. findElementInCommonAncestor(element, selector, maxIterations) {
  103. const eureka = queryOne(selector, element);
  104. if (eureka) {
  105. return eureka;
  106. }
  107. if (element.classList.contains('ecl-card__body')) {
  108. maxIterations += 1;
  109. }
  110. if (element === document.documentElement || maxIterations <= 0) {
  111. return null;
  112. }
  113. return this.findElementInCommonAncestor(
  114. element.parentElement,
  115. selector,
  116. maxIterations - 1,
  117. );
  118. }
  119. /**
  120. * Convert a path to a full url.
  121. *
  122. * @param {String} href
  123. */
  124. static convertToFullURL(href) {
  125. if (href.startsWith('http://') || href.startsWith('https://')) {
  126. return href;
  127. }
  128. const baseUrl = new URL(window.location.href);
  129. const fullUrl = new URL(href, baseUrl);
  130. return fullUrl.href;
  131. }
  132. /**
  133. * Destroy component.
  134. */
  135. destroy() {
  136. if (this.attachClickListener && this.picture) {
  137. this.picture.removeEventListener('click', this.linkto);
  138. }
  139. if (this.element) {
  140. this.element.removeAttribute('data-ecl-auto-initialized');
  141. ECL.components.delete(this.element);
  142. }
  143. }
  144. }
  145. export default ContentBlock;