content-block.js

import { queryOne } from '@ecl/dom-utils';

/**
 * @param {HTMLElement} element DOM element for component instantiation and scope
 * @param {Object} options
 * @param {Boolean} options.attachClickListener Whether or not to bind click events
 * @param (String) options.targetSelector The selector of the element where to attach the click listener
 * @param (String) options.titleSelector  The selector of the element containing the link
 * @param (Integer) options.maxIterations Maximum number of ancestors to look for the element
 */
export class ContentBlock {
  /**
   * @static
   * Shorthand for instance creation and initialisation.
   *
   * @param {HTMLElement} root DOM element for component instantiation and scope
   *
   * @return {ContentBlock} An instance of ContentBlock.
   */
  static autoInit(root, { CONTENT_BLOCK: defaultOptions = {} } = {}) {
    const contentBlock = new ContentBlock(root, defaultOptions);
    contentBlock.init();
    root.ECLContentBlock = contentBlock;
    return contentBlock;
  }

  constructor(
    element,
    {
      targetSelector = '[data-ecl-picture-link]',
      titleSelector = '[data-ecl-title-link]',
      attachClickListener = true,
      maxIterations = 1,
      withTitleAttr = false,
    } = {},
  ) {
    // Check element
    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
      throw new TypeError(
        'DOM element should be given to initialize this widget.',
      );
    }

    this.element = element;

    // Options
    this.targetSelector = targetSelector;
    this.titleSelector = titleSelector;
    this.attachClickListener = attachClickListener;
    this.maxIterations = maxIterations;
    this.withTitleAttr = withTitleAttr;

    // Bind `this` for use in callbacks
    this.linkTo = this.linkTo.bind(this);
    this.findElementInCommonAncestor =
      this.findElementInCommonAncestor.bind(this);
  }

  /**
   * Initialise component.
   */
  init() {
    if (!ECL) {
      throw new TypeError('Called init but ECL is not present');
    }
    ECL.components = ECL.components || new Map();

    this.picture = this.findElementInCommonAncestor(
      this.element,
      this.targetSelector,
      this.maxIterations,
    );

    // Exit early if no picture is present.
    if (!this.picture) {
      return;
    }

    this.title = queryOne(this.titleSelector, this.element);
    this.linkEl = this.title ? queryOne('a', this.title) : false;
    if (this.linkEl) {
      this.picture.style.cursor = 'pointer';
      const img = queryOne('img', this.picture);
      if (img && this.withTitleAttr) {
        img.title = this.constructor.convertToFullURL(
          this.linkEl.getAttribute('href'),
        );
      }

      if (this.attachClickListener) {
        this.picture.addEventListener('click', this.linkTo);
      }
    }

    this.element.setAttribute('data-ecl-auto-initialized', true);
    ECL.components.set(this.element, this);
  }

  /**
   * Redirect the user to the desired url.
   */
  linkTo() {
    if (this.linkEl) {
      // Click the linking element.
      this.linkEl.click();
    }
  }

  /**
   * Find an element in a common ancestor.
   *
   * @param {HTMLElement} element
   * @param {string} selector
   */
  findElementInCommonAncestor(element, selector, maxIterations) {
    const eureka = queryOne(selector, element);
    if (eureka) {
      return eureka;
    }

    if (element.classList.contains('ecl-card__body')) {
      maxIterations += 1;
    }

    if (element === document.documentElement || maxIterations <= 0) {
      return null;
    }

    return this.findElementInCommonAncestor(
      element.parentElement,
      selector,
      maxIterations - 1,
    );
  }

  /**
   * Convert a path to a full url.
   *
   * @param {String} href
   */
  static convertToFullURL(href) {
    if (href.startsWith('http://') || href.startsWith('https://')) {
      return href;
    }

    const baseUrl = new URL(window.location.href);
    const fullUrl = new URL(href, baseUrl);
    return fullUrl.href;
  }

  /**
   * Destroy component.
   */
  destroy() {
    if (this.attachClickListener && this.picture) {
      this.picture.removeEventListener('click', this.linkto);
    }
    if (this.element) {
      this.element.removeAttribute('data-ecl-auto-initialized');
      ECL.components.delete(this.element);
    }
  }
}

export default ContentBlock;