refreshless-8.x-1.x-dev/modules/refreshless_turbo_gin/js/dropbutton.js

modules/refreshless_turbo_gin/js/dropbutton.js
/**
 * @file
 * Gin dropbutton replacement for RefreshLess.
 *
 * Gin's dropbutton behaviour does not have a detach, and also attaches
 * anonymous, non-namespaced event subscribers to the window which can't be
 * removed directly. This solves both of those problems by rewriting as a class
 * that supports detaching/destroying the instance.
 */
(function(html, Drupal, $, once) {

  'use strict';

  /**
   * Our event namespace.
   *
   * @type {String}
   *
   * @see https://learn.jquery.com/events/event-basics/#namespacing-events
   */
  const eventNamespace = 'gin-dropbutton';

  /**
   * once() identfier.
   *
   * This is the same as Gin's behaviour. Probably doesn't matter, but might as
   * well play it safe and match the original.
   *
   * @type {String}
   */
  const onceName = 'ginDropbutton';

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

  /**
   * Property name on dropbutton elements that our core instance is saved to.
   *
   * @type {String}
   */
  const corePropName = 'drupalDropButton';

  /**
   * Represents a JavaScript-enhanced dropbutton for the Gin theme.
   */
  class GinDropButton {

    /**
     * The DropButton instance we're wrapping.
     *
     * @type {DropButton}
     */
    #wrappedInstance;

    /**
     * The second level list Gin wraps all secondary actions under.
     *
     * @type {jQuery}
     *
     * @see gin/templates/form/links--dropbutton.html.twig
     */
    #$menu;

    constructor(wrappedInstance) {

      this.#wrappedInstance = wrappedInstance;

      this.#$menu = this.$dropbutton.find('.dropbutton__items');

      this.#bindEventHandlers();

    }

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

      this.#unbindEventHandlers();

    }

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

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

      $(window).on({
        [`scroll.${eventNamespace}`]: this.#resizeAndScrollHandler,
        [`resize.${eventNamespace}`]: this.#resizeAndScrollHandler,
      });

    }

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

      this.$toggle.off(`click.${eventNamespace}`);

      $(window).off(`.${eventNamespace}`, this.#resizeAndScrollHandler);

    }

    #resizeAndScrollHandler = (event) => Drupal.debounce(this.update(), 100);

    update(onlyIfOpen = true) {

      if (onlyIfOpen === true && this.#wrappedInstance.isOpen() === false) {
        return;
      }

      const dir = html.dir ?? 'ltr';

      const $menuContainer = this.$menu.parent();

      const toggleHeight = this.$primaryAction.outerHeight(true);

      const menuWidth = this.$menu.outerWidth(true);
      const menuHeight = this.$menu.outerHeight(true);

      const boundingRect = $menuContainer[0].getBoundingClientRect();

      const windowWidth = $(window).width();
      const windowHeight = $(window).height();

      const spaceBelow = windowHeight - boundingRect.bottom;
      const spaceLeft = boundingRect.left;
      const spaceRight = windowWidth - boundingRect.right;

      const menuStyles = {
        position: 'fixed',
      };

      const leftAlignStyles = {
        left: `${boundingRect.left}px`,
        right: 'auto'
      };

      const rightAlignStyles = {
        left: 'auto',
        right: `${windowWidth - boundingRect.right}px`
      };

      if (dir === 'ltr') {

        if (spaceRight >= menuWidth) {

          Object.assign(menuStyles, leftAlignStyles);

        } else {

          Object.assign(menuStyles, rightAlignStyles);

        }

      } else {

        if (spaceLeft >= menuWidth) {

          Object.assign(menuStyles, rightAlignStyles);

        } else {

          Object.assign(menuStyles, leftAlignStyles);

        }

      }

      if (spaceBelow >= menuHeight) {

        menuStyles.top = `${boundingRect.bottom}px`;

      } else {

        menuStyles.top = `${
          boundingRect.top - toggleHeight - menuHeight
        }px`;

      }

      this.$menu.css(menuStyles);

    }

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

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

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

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

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

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

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

  }

  Drupal.behaviors.ginDropbutton = {

    attach(context, settings) {

      $(once(
        onceName, '.dropbutton-multiple:has(.dropbutton--gin)', context,
      )).each((i, element) => {

        $(element).prop(ginPropName, new GinDropButton($(element).prop(
          corePropName,
        )));

      });

    },
    detach(context, settings, trigger) {

      $(once.remove(
        // Note that this can't use the '.dropbutton-multiple' selector because
        // that will never match as the class will have already been removed by
        // the dropbutton destroy() method at this point. Instead, use the
        // '.dropbutton-wrapper' selector that is left untouched.
        onceName, '.dropbutton-wrapper:has(.dropbutton--gin)', context,
      )).each((i, element) => {

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

        $(element).removeProp(ginPropName);

      });

    },

  };

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

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

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