mercury_editor-2.0.x-dev/source/js/dialog.drupal.js

source/js/dialog.drupal.js
(($, Drupal, drupalSettings) => {
  // TODO: This does not seem to be used, but keeping for reference.
  // drupalSettings.mercuryDialog = {
  //   autoOpen: true,
  //   autoResize: false,
  //   dialogClass: '',
  //   buttonClass: 'button',
  //   buttonPrimaryClass: 'button--primary',
  // };

  function dispatchDialogEvent(eventType, dialog, element, settings) {
    // Use the jQuery if the DrupalDialogEvent is not defined for BC.
    if (typeof DrupalDialogEvent === 'undefined') {
      $(window).trigger(`dialog:${eventType}`, [dialog, $(element), settings]);
    } else {
      // eslint-disable-next-line no-undef
      const event = new DrupalDialogEvent(eventType, dialog, settings || {});
      element.dispatchEvent(event);
    }
  }

  Drupal.mercuryDialog = (element, baseOptions) => {
    // Override the jQuery dialog method to return the element, preventing
    // uncaught errors from core jQuery dialog being thrown.
    const $element = $(element);
    $element.dialog = () => $element;

    let dialogElement;
    let undef;

    // The main dialog object that will be decorated with functions and returned.
    const dialog = {
      open: false,
      returnValue: undef,
    };

    /**
     * Determines which element to append the dialog to. Defaults to the <body>
     * @param {Element} appendTo The element to append the dialog to.
     * @return {jQuery} The element to append the dialog to.
     */
    function _appendTo(appendTo) {
      if (appendTo && (appendTo.jquery || appendTo.nodeType)) {
        return $(appendTo);
      }
      return $(document)
        .find(appendTo || 'body')
        .eq(0);
    }

    /**
     * Create a button pane similar to jQuery UI dialog.
     * @param {Array} buttons The buttons to create.
     */
    function _createButtonPane(buttons) {
      const existing = dialogElement.querySelector('.me-dialog__buttonpane');
      if (existing) {
        existing.remove();
      }
      const uiDialogButtonPane = document.createElement('div');
      uiDialogButtonPane.setAttribute('slot', 'footer');
      uiDialogButtonPane.classList.add('me-dialog__buttonpane');
      dialogElement.appendChild(uiDialogButtonPane);
      if (
        $.isEmptyObject(buttons) ||
        (Array.isArray(buttons) && !buttons.length)
      ) {
        return;
      }
      buttons.forEach((props) => {
        const button = document.createElement('button');
        button.classList = props.class;
        button.classList.add('button');
        button.appendChild(document.createTextNode(props.text));
        button.addEventListener('click', props.click);
        uiDialogButtonPane.appendChild(button);
      });
    }

    /**
     * ResizeObserver callback that runs when the shadow dialog element resizes.
     *
     * @param {ResizeObserverEntry[]} entries The entries to observe.
     */
    function onDockResize(entries) {
      entries.forEach((entry) => {
        const dockElement = entry.target;
        const dockDirection = dockElement.getAttribute('data-dock');
        if (!dockElement.open || !dockDirection || dockDirection === 'none') {
          return;
        }
        const { width } = entry.contentRect;
        const { height } = entry.contentRect;
        // Dispatch custom event with resize data
        const resizeEvent = new CustomEvent('mercury:dockResize', {
          detail: { width, height },
          bubbles: true,
        });
        dialogElement.dispatchEvent(resizeEvent);
      });
    }
    const dockObserver = new ResizeObserver(onDockResize);

    /**
     * MutationObserver callback that will start the resize observer when the dialog opens.
     */
    function onDialogMutate() {
      // @todo: We may only want to attach this if the dialog is transitioning to an open state.
      dockObserver.observe(dialogElement.shadowRoot.querySelector('dialog'));
    }
    const dialogMutationObserver = new MutationObserver(onDialogMutate);

    /**
     * Converts a numeric only value to a px based CSS value.
     * @param {*} value The value to convert.
     * @return {string} A CSS length value.
     */
    function getCSSLength(value) {
      if (typeof value === 'undefined' || !/^\d+$/.test(value)) {
        return value;
      }
      return `${value}px`;
    }

    /**
     * Apply options to the <mercury-dialog> element.
     * @param {Object} options The options to apply.
     * @return {Element} The <mercury-dialog> element.
     */
    function applyOptions(options) {
      // Attributes that will be set on the <mercury-dialog> element.
      const attributeOptions = [
        'title',
        'modal',
        'dock',
        'push',
        'resizable',
        'moveable',
      ];
      // Set these options as HTML attributes on the element.
      attributeOptions.forEach((option) => {
        if (typeof options[option] !== 'undefined') {
          dialogElement.setAttribute(option, options[option]);
        }
      });

      // Set the dialog classes.
      if (options.dialogClass) {
        dialogElement.classList.add(...options.dialogClass.split(' '));
      }
      const dialogElements = {
        'ui-dialog': dialogElement,
        'ui-dialog-titlebar': dialogElement.shadowRoot.querySelector(
          'header[slot="header"]',
        ),
        'ui-dialog-title': dialogElement.shadowRoot.querySelector(
          'h1.me-dialog__title',
        ),
        'ui-dialog-content': dialogElement.shadowRoot.querySelector('main'),
        'ui-dialog-buttonpane':
          dialogElement.shadowRoot.querySelector('div[slot="footer"]'),
        'ui-dialog-buttonset':
          dialogElement.shadowRoot.querySelector('div[slot="footer"]'),
      };
      Object.keys(options.classes || {}).forEach((key) => {
        if (dialogElements[key] && options.classes[key]) {
          dialogElements[key].classList.add(...options.classes[key].split(' '));
        }
      });

      const dock = options.dock || dialogElement.getAttribute('dock');
      if (dock === 'right') {
        // Options for the main Mercury Editor tray.
        const isTrayCollapsed =
          localStorage.getItem('mercury-dialog-dock-collapsed') === 'true';

        if (!isTrayCollapsed) {
          const dialogWidth = options.width;
          const dialogHeight = options.height;
          if (
            options?.defaultWidth &&
            !document.documentElement.style.getPropertyValue(
              '--me-dialog-dock-right-width',
            )
          ) {
            document.documentElement.style.setProperty(
              '--me-dialog-dock-right-width',
              getCSSLength(options.defaultWidth),
            );
          }
          if (dialogWidth) {
            document.documentElement.style.setProperty(
              '--me-dialog-dock-right-width',
              getCSSLength(dialogWidth),
            );
          }
          if (dialogHeight) {
            document.documentElement.style.setProperty(
              '--me-dialog-dock-right-height',
              getCSSLength(dialogHeight),
            );
          }
        } else {
          document.documentElement.style.setProperty(
            '--me-dialog-dock-right-width',
            '10px',
          );
        }
      } else {
        // Options for all other dialogs.
        if (options.width) {
          document.documentElement.style.setProperty(
            '--me-dialog-width',
            getCSSLength(options.width),
          );
        }

        if (options.height) {
          document.documentElement.style.setProperty(
            '--me-dialog-height',
            getCSSLength(options.height),
          );
        }
      }

      // TODO: Determine if we need to persist the width and height of docked dialogs.
      // if (options.dock) {
      //   if (savedWidth && ['right', 'left'].includes(options.dock)) {
      //     dialogElement.setAttribute('width', savedWidth);
      //   }
      //   if (savedHeight && ['top', 'bottom'].includes(options.dock)) {
      //     dialogElement.setAttribute('height', savedHeight);
      //   }
      // }

      if (options.drupalAutoButtons && !options.buttons) {
        options.buttons = Drupal.behaviors.mercuryDialog.prepareDialogButtons(
          $(dialogElement),
        );
      }

      if (options.buttons && options.buttons.length) {
        _createButtonPane(options.buttons);
      }

      return dialogElement;
    }

    /**
     * Initializes the dialog element.
     * @param {Object} settings The settings to apply to the dialog.
     */
    function init(settings) {
      // Wrap the element in a <mercury-dialog> if it isn't already.
      if (element.tagName !== 'MERCURY-DIALOG') {
        const wrapper = $('<mercury-dialog>')
          .append($element)
          .appendTo(_appendTo(settings.appendTo));
        [dialogElement] = wrapper;
      } else {
        dialogElement = element;
      }
      applyOptions(settings);
    }

    /**
     * Closes the dialog element.
     * @param {Mixed} value The value to return from the dialog.
     */
    function closeDialog(value) {
      dispatchDialogEvent('beforeclose', dialog, $element.get(0));
      // Stop observing height and width changes.
      dockObserver.disconnect();
      dialogMutationObserver.disconnect();
      Drupal.detachBehaviors(element, null, 'unload');
      element.close();
      dialog.returnValue = value;
      dispatchDialogEvent('afterclose', dialog, $element.get(0));
      $element.remove();
    }

    /**
     * Initializes and opens a dialog element.
     * @param {Object} settings Dialog settings mimicking jQuery UI for compatibility.
     */
    function openDialog(settings) {
      settings = {
        ...drupalSettings.dialog,
        ...drupalSettings.mercuryEditor,
        ...baseOptions,
        ...settings,
      };
      dispatchDialogEvent('beforecreate', dialog, $element.get(0), settings);
      init(settings);
      dialogElement[settings.modal ? 'showModal' : 'show']();
      // Set autoResize to false to prevent Drupal core's jQuery dialog from
      // attempting to resize, which would throw an error.
      const originalResizeSetting = settings.autoResize;
      settings.autoResize = false;
      dispatchDialogEvent('aftercreate', dialog, $element.get(0), settings);
      settings.autoResize = originalResizeSetting;
      // Add a mutation observer to the dialog element.
      dialogMutationObserver.observe(dialogElement, {
        childList: true,
        attributes: true,
      });
      dialogElement.addEventListener('close', () => {
        closeDialog();
      });
    }

    dialog.show = () => {
      openDialog({ modal: false });
    };

    dialog.showModal = () => {
      openDialog({ modal: true });
    };

    dialog.applyOptions = (dialogOptions) => {
      init(dialogOptions);
    };

    dialog.close = closeDialog;

    return dialog;
  };

  Drupal.behaviors.mercuryDialog = {
    attach: (context) => {
      // Provide a known 'drupal-mercury-dialog' DOM element for Drupal-based modal
      // dialogs. Non-modal dialogs are responsible for creating their own
      // elements, since there can be multiple non-modal dialogs at a time.

      if (!$('#drupal-mercury-dialog').length) {
        // Add 'ui-front' jQuery UI class so jQuery UI widgets like autocomplete
        // sit on top of dialogs. For more information see
        // http://api.jqueryui.com/theming/stacking-elements/.
        // @todo: .ui-front just sets a z-index of 100 which is not high enough
        // to overlay Gin's Toolbar.
        $(
          '<mercury-dialog id="drupal-mercury-dialog"></mercury-dialog>',
        ).appendTo('body');
      }

      // Special behaviors specific when attaching content within a dialog.
      // These behaviors usually fire after a validation error inside a dialog.
      const $dialog = $(context).closest('mercury-dialog');
      if ($dialog.length) {
        $dialog.trigger('dialogButtonsChange');
      }
    },

    prepareDialogButtons: function prepareDialogButtons($dialog) {
      const buttons = [];
      const dialogElement = $dialog[0];
      const allFormActions = dialogElement.querySelectorAll('.form-actions');
      if (allFormActions.length === 0) {
        return buttons;
      }
      const formActions = allFormActions[allFormActions.length - 1];
      formActions
        .querySelectorAll('input[type=submit], a.button, a.action-link')
        .forEach((button) => {
          button.style.display = 'none';
          buttons.push({
            text: button.textContent || button.getAttribute('value'),
            class: button.className,
            click: function click(e) {
              if (button.tagName.toLowerCase() === 'a') {
                button.click();
              } else {
                ['mousedown', 'mouseup', 'click'].forEach((type) => {
                  button.dispatchEvent(
                    new MouseEvent(type, {
                      bubbles: true,
                      cancelable: true,
                      view: window,
                    }),
                  );
                });
              }
              e.preventDefault();
            },
          });
        });

      return buttons;
    },
  };

  // Moves Layout Paragraphs form buttons into the dialog button pane.
  function moveFormButtonsToDialog(event, dialog, $dialog) {
    if ($dialog[0].tagName !== 'MERCURY-DIALOG') {
      return;
    }
    if ($dialog.attr('id').indexOf('lpb-dialog-') === 0) {
      const buttons =
        Drupal.behaviors.mercuryDialog.prepareDialogButtons($dialog);
      if (buttons.length) {
        Drupal.mercuryDialog($dialog[0]).applyOptions({ buttons });
      }
    }
  }
  $(window).on('dialog:aftercreate', moveFormButtonsToDialog);

  /**
   * ResizeObserver callback that resizes the parent iframe based on
   * the height of the child document html element.
   *
   * @param {HTMLIFrameElement} iframe The iframe element to resize.
   * @return {Function} The callback function.
   */
  function onBodyResize(iframe) {
    return (entries) => {
      // Resize the parent iframe based on the html's border box height.
      if (iframe && entries.length) {
        iframe.style.height = `${entries[0].borderBoxSize[0].blockSize + 1}px`;
        iframe.style.width = `${entries[0].borderBoxSize[0].inlineSize + 1}px`;
      }
    };
  }

  /**
   * Set a max-width on the iframe's <body> to match the dialog's
   * max-width to prevent horizontal scrolling.
   *
   * @param {HTMLIFrameElement} iframe
   *   The iframe element within a mercury dialog.
   * @param {HTMLBodyElement} framedBody
   *   The body element within the iframe.
   */
  function setFrameBodyMaxWidth(iframe, framedBody) {
    const dialogStyles = window.getComputedStyle(
      iframe.closest('mercury-dialog').shadowRoot.querySelector('dialog'),
    );
    const dialogMainStyles = window.getComputedStyle(
      iframe.closest('mercury-dialog').shadowRoot.querySelector('main'),
    );
    framedBody.style.maxWidth = `calc(${dialogStyles.getPropertyValue('max-width')} - ${dialogMainStyles.getPropertyValue('padding-left')} - ${dialogMainStyles.getPropertyValue('padding-right')} - 2px)`;
  }

  /**
   * Resizes an iFrame to match the height of its inner <body> element.
   * @param {HTMLIFrameElement} iframe The iframe element to resize.
   */
  function resizeIframe(iframe) {
    const framedBody = iframe.contentWindow.document.body;
    framedBody.style.width = 'max-content';
    framedBody.style.height = 'fit-content';

    setFrameBodyMaxWidth(iframe, framedBody);

    // Observe changes to the iframe's inner <body> dimensions.
    new ResizeObserver(onBodyResize(iframe)).observe(framedBody, {
      box: 'border-box',
    });
  }

  function updateIframeSize(event, dialog, $dialog) {
    if ($dialog[0].tagName !== 'MERCURY-DIALOG') {
      return;
    }

    const iframe = $dialog[0].querySelector('iframe');
    if (!iframe) {
      return;
    }

    $dialog[0].style.setProperty('--me-dialog-height-default', 'fit-content');

    const framedBody = iframe?.contentWindow?.document?.body;
    iframe.onload = () => {
      resizeIframe(iframe);
      setFrameBodyMaxWidth(iframe, framedBody);
    };
    if (framedBody) {
      window.addEventListener('resize', () => {
        setFrameBodyMaxWidth(iframe, framedBody);
      });
    }
  }
  $(window).on('dialog:aftercreate', updateIframeSize);

  // Store open modals.
  const modalStack = [];

  // The following event handlers are used to manage the modal stack.
  // Since native dialog['modal'] elements live in the browser's top-level,
  // we need to make sure any jQuery ui modals that are opened from within
  // a mercury-dialog element get nested within the top-level modal.
  // Otherwise, the jQuery dialog will be obscured by the mercury-dialog modal.
  // See https://developer.chrome.com/blog/what-is-the-top-layer/
  function addModalToStack(event, dialog, $dialog) {
    if ($dialog[0].tagName !== 'MERCURY-DIALOG') {
      return;
    }
    if (
      $dialog[0].hasAttribute('modal') &&
      $dialog[0].getAttribute('modal') !== 'false'
    ) {
      modalStack.push($dialog);
    }
  }
  $(window).on('dialog:aftercreate', addModalToStack);

  function removeModalFromStack(event, dialog, $dialog) {
    if ($dialog[0].tagName !== 'MERCURY-DIALOG') {
      return;
    }
    if (
      $dialog[0].hasAttribute('modal') &&
      $dialog[0].getAttribute('modal') !== 'false'
    ) {
      const index = modalStack.indexOf($dialog);
      if (index > -1) {
        modalStack.splice(index, 1);
      }
    }
  }
  $(window).on('dialog:beforeclose', removeModalFromStack);

  function nestDialogInModal(event, dialog, $dialog) {
    if ($dialog[0].tagName !== 'MERCURY-DIALOG') {
      return;
    }
    if (modalStack.length > 0) {
      const $parent = $dialog.parent('.ui-dialog');
      const $overlay = $parent.next('.ui-widget-overlay');
      modalStack.slice(-1)[0].append([$parent, $overlay]);
    }
  }
  $(window).on('dialog:aftercreate', nestDialogInModal);
})(jQuery, Drupal, drupalSettings);

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

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