refreshless-8.x-1.x-dev/modules/refreshless_turbo/js/overrides/dropbutton.js

modules/refreshless_turbo/js/overrides/dropbutton.js
/**
 * @file
 * Drupal dropbutton replacement for RefreshLess.
 *
 * The biggest issue this solves is that core's dropbutton fails to open when
 * revisiting a cached page with Turbo, most likely because core's dropbutton
 * binds a delegated click handler on the <body> and does not detach it. While
 * this could be solved indirectly, actually doing so reliably wasn't working,
 * so a rewrite was easier than trying to fix the mess the lack of detach was
 * causing.
 *
 * Note that anything that's exposed by this must maintain specific names to
 * ensure maximum compatibility with core and contrib.
 *
 * @todo Expose Drupal.DropButton and Drupal.DropButton.dropbuttons if anything
 *   actually uses them. If so, use a Proxy object to keep things separated.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
 *
 * @see https://stackoverflow.com/questions/69860820/is-there-a-way-to-proxy-intercept-all-methods-of-a-class-in-javascript/69860903#69860903
 *   Details returning a Proxy object in a class constructor which is apparently
 *   valid.
 */
(function(html, Drupal, $, once) {

  'use strict';

  /**
   * Our event namespace.
   *
   * This should be 'dropbutton' for backwards compatibility with core and
   * contrib.
   *
   * @type {String}
   *
   * @see https://learn.jquery.com/events/event-basics/#namespacing-events
   */
  const eventNamespace = 'dropbutton';

  /**
   * once() identfier.
   *
   * Same naming restriction as with eventNamespace.
   *
   * @type {String}
   */
  const onceName = eventNamespace;

  /**
   * Property name on dropbutton elements that we save our instance to.
   *
   * @type {String}
   */
  const propName = 'drupalDropButton';

  /**
   * Represents a JavaScript-enhanced dropbutton.
   */
  class DropButton {

    /**
     * The dropbutton element root.
     *
     * @type {jQuery}
     */
    #$element;

    /**
     * The list element containing all dropbutton actions.
     *
     * @type {jQuery}
     */
    #$list;

    /**
     * All dropbutton actions.
     *
     * @type {jQuery}
     */
    #$actions;

    /**
     * The primary dropbutton action.
     *
     * @type {jQuery}
     */
    #$primaryAction;

    /**
     * The secondary dropbutton actions, if any.
     *
     * @type {jQuery}
     */
    #$secondaryActions;

    /**
     * The dropbutton toggle element, if any.
     *
     * @type {jQuery}
     */
    #$toggle = $();

    /**
     * Time in milliseconds to delay closing the dropbutton.
     *
     * @type {Number}
     */
    #closeDelay = 500;

    /**
     * setTimeout() identifier, if any.
     *
     * @type {Number|Null}
     */
    #timerId = null;

    /**
     * Dropbutton options object.
     *
     * @type {Object}
     */
    #options;

    constructor(element, settings) {

      this.#$element = $(element);

      this.#$list = this.#$element.find('.dropbutton');

      this.#$actions = this.#$list.find('li').addClass('dropbutton-action');

      this.#$primaryAction = this.#$actions.first();

      this.#$secondaryActions = this.#$actions.not(
        this.#$primaryAction,
      ).addClass('secondary-action');

      // Merge defaults with settings.
      this.#options = $.extend(
        { title: Drupal.t('List additional actions') },
        settings,
      );

      this.#$element.addClass(
        this.hasMultiple() ? 'dropbutton-multiple' : 'dropbutton-single'
      );

      if (this.hasMultiple()) {

        this.#$toggle = $(Drupal.theme(
          'dropbuttonToggle', this.#options,
        )).insertAfter(this.#$primaryAction);

      }

      this.#bindEventHandlers();

    }

    /**
     * Destroy this instance by detaching from the element.
     */
    destroy() {

      this.#unbindEventHandlers();

      this.#$element.removeClass(['dropbutton-multiple', 'dropbutton-single']);

      this.#$actions.removeClass(['dropbutton-action', 'secondary-action']);

      this.#$toggle.remove();

    }

    /**
     * Whether this dropbutton has multiple actions.
     *
     * @return {Boolean}
     *   True if the dropbutton has more than one action, and false if it has
     *   only one action.
     */
    hasMultiple() {
      return this.#$actions.length > 1;
    }

    /**
     * Bind all of our event handlers.
     */
    #bindEventHandlers() {

      if (!this.hasMultiple()) {
        return;
      }

      this.#$element.on({
        [`mouseenter.${eventNamespace}`]: (event) => {
          this.#openHandler(event);
        },
        [`mouseleave.${eventNamespace}`]: (event) => {
          this.#closeHandler(event);
        },
        [`focusin.${eventNamespace}`]: (event) => {
          this.#openHandler(event);
        },
        [`focusout.${eventNamespace}`]: (event) => {
          this.#closeHandler(event);
        },
      });

      this.#$toggle.on(`click.${eventNamespace}`, (event) => {
        this.#toggleClickHandler(event);
      });

    }

    /**
     * Unbind all of our event handlers.
     */
    #unbindEventHandlers() {

      if (!this.hasMultiple()) {
        return;
      }

      this.#$element.add(this.#$toggle).off(`.${eventNamespace}`);

    }

    /**
     * Open event handler.
     *
     * @param {jQuery.Event} event
     */
    #openHandler(event) {

      if (typeof this.#timerId !== 'null') {
        window.clearTimeout(this.#timerId);
      }

    }

    /**
     * Close event handler.
     *
     * @param {jQuery.Event} event
     */
    #closeHandler(event) {

      this.#timerId = window.setTimeout(() => {
        this.close();
      }, this.#closeDelay);

    }

    /**
     * Toggle click event handler.
     *
     * @param {jQuery.Event} event
     */
    #toggleClickHandler(event) {

      this.toggle();

      event.preventDefault();

    }

    /**
     * Whether the dropbutton menu is currently open.
     *
     * @return {Boolean}
     *   True if the dropbutton menu is currently open, false otherwise.
     */
    isOpen() {
      return this.#$element.hasClass('open');
    }

    /**
     * Toggle this dropbutton open and closed.
     *
     * @param {boolean} show
     *   Force the dropbutton to open by passing true or to close by passing
     *   false.
     */
    toggle(show) {

      const isBool = typeof show === 'boolean';

      show = isBool ? show : !this.isOpen();

      this.#$element.toggleClass('open', show);

    }

    /**
     * Open this dropbutton.
     */
    open() {
      this.toggle(true);
    }

    /**
     * Close this dropbutton.
     */
    close() {
      this.toggle(false);
    }

    get $dropbutton() {
      return this.#$element;
    }

    get $toggle() {
      return this.#$toggle;
    }

    get $list() {
      return this.#$list;
    }

    get $actions() {
      return this.#$actions;
    }

    get $primaryAction() {
      return this.#$primaryAction;
    }

    get $secondaryActions() {
      return this.#$secondaryActions;
    }

  }

  /**
   * Dropbutton toggle element theme implementation.
   *
   * @param {Object} options
   *   Options object.
   * @param {String} [options.title]
   *   The button text.
   *
   * @return {String}
   *   A string representing a DOM fragment.
   */
  Drupal.theme.dropbuttonToggle = (options) => {
    return `<li class="dropbutton-toggle"><button type="button"><span class="dropbutton-arrow"><span class="visually-hidden">${options.title}</span></span></button></li>`;
  };

  Drupal.behaviors.dropButton = {

    attach(context, settings) {

      $(once(onceName, '.dropbutton-wrapper', context)).each((i, element) => {

        $(element).prop(propName, new DropButton(element, settings.dropbutton));

      });

    },
    detach(context, settings, trigger) {

      $(once.remove(
        onceName, '.dropbutton-wrapper', context,
      )).each((i, element) => {

        $(element).prop(propName)?.destroy();

        $(element).removeProp(propName);

      });

    },

  };

})(document.documentElement, Drupal, jQuery, once);

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

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