commercetools-8.x-1.2-alpha1/modules/commercetools_content/js/ajaxify_blocks.js

modules/commercetools_content/js/ajaxify_blocks.js
(function (Drupal) {
  // Global data if selector
  const productIndexSelector = 'data-ct-product-index';
  const dataBlockId = 'data-ct-block-id';
  const systemBlockSelector = '[data-ct-is-system-block]';
  const systemBlockForceUpdateSelector = 'data-ct-ajaxify-system-block';
  const containerSelector = '[data-ct-ajaxify]';
  const behaviors = {
    attach(context) {
      // Universal container selector and derived link selector.
      const ajaxifySelector = `${containerSelector} a:not(.ct-ajaxify-ignore)`;

      // Attach form handlers
      this.attachFormHandlers(containerSelector, context);

      // Attach link handlers
      this.attachLinksHandler(ajaxifySelector, context);
    },

    /**
     * Links handler attachment checks only suitable links
     * @param {string} selector - Selector for links
     * @param context - context
     */
    attachLinksHandler(selector, context) {
      if (document.__ctAjaxified) {
        return;
      }
      document.__ctAjaxified = true;
      document.addEventListener('click', (event) => {
        if (event.target.tagName !== 'A') {
          return;
        }

        const href = event.target.getAttribute('href');
        if (!this.isUrlAjaxifiable(href)) return;
        // Find the closest container
        const container =
          event.target.closest(`${containerSelector}`) || document;

        this.handleClick(event.target, container);
        event.preventDefault();
      });
    },

    /**
     * Determines if a link should be ajaxified.
     * @param {string} href - The link URL.
     * @return {boolean} True if the link should be ajaxified, false otherwise.
     */
    isUrlAjaxifiable(href) {
      const currentPath = this.getFullCurrentPath();
      // Resolve relative paths (e.g., "?page=...") into absolute paths for comparison.
      if (href.startsWith('?')) {
        href = `${currentPath}${href}`;
      }
      // Extracts only the path parameter from the url without GET parameters.
      const ajaxLinkPattern = /(\/[^?]*)(\?.*)?/;
      const result = ajaxLinkPattern.exec(href);
      if (result !== null && result.at(1) === currentPath) {
        return true;
      }
      // Exclude links that do not meet criteria.
      return false;
    },

    /**
     * Gets full current path including baseUrl.
     */
    getFullCurrentPath() {
      return drupalSettings.path.baseUrl + drupalSettings.path.currentPath;
    },

    /**
     * Performs the click action on an ajaxified link
     * @param {HTMLElement} clickedElement - The clicked link
     * @param {HTMLElement} context - element from context
     */
    handleClick(clickedElement, context) {
      let targetUrl = clickedElement.getAttribute('href');
      // Add currentPath to relative links
      if (targetUrl.startsWith('?')) {
        targetUrl = this.getFullCurrentPath() + targetUrl;
      }

      const productIndex = this.getCtProductIndex(clickedElement);
      // Collect block IDs for the current product index
      const blockIds = this.getBlockIds(productIndex);

      // Check if force update system block required.
      if (context.hasAttribute(systemBlockForceUpdateSelector)) {
        blockIds.push(this.getSystemMainBlockId());
      }

      // Build ajax query with targetUrl and blocks
      this.updateList(targetUrl, blockIds);
      // Update browser history and trigger AJAX
      this.updateBrowserHistory(targetUrl);
    },

    /**
     * Attach form handlers for filters and search
     */
    attachFormHandlers(selector, context) {
      const formSelector = `${selector} form`;
      const forms = context.querySelectorAll(formSelector, context);

      forms.forEach((form) => {
        form.addEventListener('submit', (e) => {
          e.preventDefault();
          const container = form.closest(selector) || document;
          this.handleFormSubmit([`${selector} form`], container);
        });
      });
    },

    /**
     * Handle form submission
     */
    handleFormSubmit(formSelectors, scope) {
      const formsData = this.getFormsData(formSelectors, scope);
      const filterParams = new URLSearchParams(formsData);

      // todo re-think approach if there a few forms on the page.
      const controlledKeys = this.getFormKeys(formSelectors[0]);

      // Merge with current URL parameters, preserving all existing filters
      const urlParams = this.mergeQueryParams(filterParams, controlledKeys);

      // Get current product index and collect block IDs
      const index = this.getCtProductIndex(scope);

      const blockIds = this.getBlockIds(index);

      // Check if force update system block required.
      if (scope.hasAttribute(systemBlockForceUpdateSelector)) {
        blockIds.push(this.getSystemMainBlockId());
      }

      // Construct the finalized URL
      const currentPath = this.getFullCurrentPath();
      const targetUrl = `${currentPath}?${urlParams.toString()}`;

      this.updateBrowserHistory(targetUrl);
      this.updateList(targetUrl, blockIds);
    },

    /**
     * Get system main block ID by data attribute.
     */
    getSystemMainBlockId() {
      const el = document.querySelector(`${systemBlockSelector}`);
      if (el) {
        return el.getAttribute(dataBlockId);
      }
    },

    /**
     * Merge new query parameters into existing query parameters.
     * If a parameter exists in both, the new value takes precedence.
     * @param {URLSearchParams} newParams - New parameters to merge (e.g., from form inputs).
     * @param controlledKeys
     * @return {URLSearchParams} - The merged query parameters.
     */
    mergeQueryParams(newParams, controlledKeys) {
      const merged = new URLSearchParams(window.location.search);
      const controlled = new Set(controlledKeys);
      const keysToDelete = [];

      // Identifies params to remove
      merged.forEach((value, key) => {
        const baseKey = key.replace(/\[\d+\]$/, '');
        if (controlled.has(baseKey)) {
          keysToDelete.push(key);
        }
      });

      keysToDelete.forEach((key) => merged.delete(key));
      // Re-add params form url
      newParams.forEach((value, key) => {
        if (value !== '') {
          merged.append(key, value);
        }
      });
      // Clean up the pager parameter from the url to jump to the first page
      // on the submit of the filter form.
      merged.delete('page');
      return merged;
    },

    /**
     * Gets unique form elements by name.
     * @param form Selector
     * @return {Set<any>}
     */
    getFormKeys(form) {
      const names = new Set();
      const ignoreFields = ['form_build_id', 'form_token', 'form_id', 'op'];
      const elements = document.querySelectorAll(`${form} [name]`);

      for (let i = 0; i < elements.length; i++) {
        const el = elements[i];
        if (!ignoreFields.includes(el.name)) {
          const uniqueName = el.name.replace(/\[\d+]$/, '');
          names.add(uniqueName);
        }
      }

      return names;
    },

    /**
     * Get product index.
     * @param context
     * @return {*}
     */
    getCtProductIndex(context) {
      const wrapperWithIndex = context.closest(`[${productIndexSelector}]`);
      return wrapperWithIndex?.getAttribute(productIndexSelector) || 0;
    },

    /**
     * Get block IDs for a given product index.
     * If no index provided, finds the index from DOM context.
     * @param {string} index - The data-ct-product-index value.
     * @return {Array} Array of block ID strings.
     */
    getBlockIds(index) {
      // If no index provided, find it from DOM context
      if (!index) {
        const indexElement = document.querySelector(
          `[${productIndexSelector}]`,
        );
        index = indexElement?.getAttribute(productIndexSelector) || '0';
      }

      return Array.from(
        document.querySelectorAll(`[${productIndexSelector}="${index}"]`),
      )
        .filter((el) => el.hasAttribute(dataBlockId))
        .map((el) => el.getAttribute(dataBlockId))
        .filter(Boolean);
    },

    /**
     * Get form data from multiple forms
     */
    getFormsData(formSelectors, scope) {
      const root = scope || document;
      const forms = root.querySelectorAll(formSelectors.join(', '));
      const formsData = new FormData();
      const ignoreFields = ['form_build_id', 'form_token', 'form_id'];

      forms.forEach((form) => {
        const formData = new FormData(form);
        formData.forEach((value, key) => {
          if (value.trim() !== '' && !ignoreFields.includes(key)) {
            formsData.set(key, value);
          }
        });
      });

      return formsData;
    },

    /**
     * Update browser history
     */
    updateBrowserHistory(targetUrl) {
      const url = new URL(targetUrl, window.location.origin);
      const path = url.pathname + url.search;
      window.history.pushState({ path }, '', path);
    },

    /**
     * Send AJAX request to update content
     */
    updateList(url, blocks) {
      const blocksQuery = encodeURIComponent(blocks.join(','));
      // Encode the targetUrl
      const encodedTargetUrl = encodeURIComponent(url);

      const ajaxSettings = {
        url: Drupal.url(
          `api/commercetools/ajax?target_url=${encodedTargetUrl}&blocks=${blocksQuery}`,
        ),
        progress: {
          type: 'fullscreen',
          message: Drupal.t('Processing...'),
        },
      };
      const ajax = Drupal.ajax(ajaxSettings);
      ajax.execute();
    },
  };
  // Attach the behavior to Drupal
  if (typeof Drupal !== 'undefined') {
    Drupal.behaviors.commercetoolsAjaxifyBlocks = behaviors;
  }
})(Drupal);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc