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 = 2,
  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 === document.documentElement || maxIterations <= 0) {
  108. return null;
  109. }
  110. return this.findElementInCommonAncestor(
  111. element.parentElement,
  112. selector,
  113. maxIterations - 1,
  114. );
  115. }
  116. /**
  117. * Convert a path to a full url.
  118. *
  119. * @param {String} href
  120. */
  121. static convertToFullURL(href) {
  122. if (href.startsWith('http://') || href.startsWith('https://')) {
  123. return href;
  124. }
  125. const baseUrl = new URL(window.location.href);
  126. const fullUrl = new URL(href, baseUrl);
  127. return fullUrl.href;
  128. }
  129. /**
  130. * Destroy component.
  131. */
  132. destroy() {
  133. if (this.attachClickListener && this.picture) {
  134. this.picture.removeEventListener('click', this.linkto);
  135. }
  136. if (this.element) {
  137. this.element.removeAttribute('data-ecl-auto-initialized');
  138. ECL.components.delete(this.element);
  139. }
  140. }
  141. }
  142. export default ContentBlock;