text: /**
   * Copyright 2019 Adobe. All rights reserved.
   * This file is licensed to you under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License. You may obtain a copy
   * of the License at http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software distributed under
   * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
   * OF ANY KIND, either express or implied. See the License for the specific language
   * governing permissions and limitations under the License.
   */
  
  import {BaseComponent} from '../../../coral-base-component';
  import {BaseOverlay} from '../../../coral-base-overlay';
  import {DragAction} from '../../../coral-dragaction';
  import {Icon} from '../../../coral-component-icon';
  import '../../../coral-component-button';
  import base from '../templates/base';
  import {commons, transform, validate, i18n} from '../../../coral-utils';
  import {Decorator} from '../../../coral-decorator';
  
  /**
   Enumeration for {@link Dialog} closable options.
  
   @typedef {Object} DialogClosableEnum
  
   @property {String} ON
   Show a close button on the dialog and close the dialog when clicked.
   @property {String} OFF
   Do not show a close button. Elements with the <code>coral-close</code> attribute will still close the dialog.
   */
  const closable = {
    ON: 'on',
    OFF: 'off'
  };
  
  /**
   Enumeration for {@link Dialog} keyboard interaction options.
  
   @typedef {Object} DialogInteractionEnum
  
   @property {String} ON
   Keyboard interaction is enabled.
   @property {String} OFF
   Keyboard interaction is disabled.
   */
  const interaction = {
    ON: 'on',
    OFF: 'off'
  };
  
  /**
   Enumeration for {@link Dialog} variants.
  
   @typedef {Object} DialogVariantEnum
  
   @property {String} DEFAULT
   A default dialog without header icon.
   @property {String} ERROR
   A dialog with an error header and icon, indicating that an error has occurred.
   @property {String} WARNING
   A dialog with a warning header and icon, notifying the user of something important.
   @property {String} SUCCESS
   A dialog with a success header and icon, indicates to the user that an operation was successful.
   @property {String} HELP
   A dialog with a question header and icon, provides the user with help.
   @property {String} INFO
   A dialog with an info header and icon, informs the user of non-critical information.
   */
  const variant = {
    DEFAULT: 'default',
    ERROR: 'error',
    WARNING: 'warning',
    SUCCESS: 'success',
    HELP: 'help',
    INFO: 'info'
  };
  
  /**
   Enumeration for {@link Dialog} backdrops.
  
   @typedef {Object} DialogBackdropEnum
  
   @property {String} NONE
   No backdrop.
   @property {String} MODAL
   A backdrop that hides the dialog when clicked.
   @property {String} STATIC
   A backdrop that does not hide the dialog when clicked.
   */
  const backdrop = {
    NONE: 'none',
    MODAL: 'modal',
    STATIC: 'static'
  };
  
  // Used to map icon with variant
  const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);
  
  // The dialog's base classname
  const CLASSNAME = '_coral-Dialog';
  // Modifier classnames
  const FULLSCREEN_CLASSNAME = `${CLASSNAME}--fullscreenTakeover`;
  
  // A string of all possible variant classnames
  const ALL_VARIANT_CLASSES = [];
  for (const variantValue in variant) {
    ALL_VARIANT_CLASSES.push(`${CLASSNAME}--${variant[variantValue]}`);
  }
  
  /**
   @class Coral.Dialog
   @classdesc A Dialog component that supports various use cases with custom content. The Dialog can be given a size by
   using the special attribute <code>[coral-dialog-size]</code> as selector.
   @htmltag coral-dialog
   @extends {HTMLElement}
   @extends {BaseComponent}
   @extends {BaseOverlay}
   */
  const Dialog = Decorator(class extends BaseOverlay(BaseComponent(HTMLElement)) {
    /** @ignore */
    constructor() {
      super();
  
      // Prepare templates
      this._elements = commons.extend(this._elements, {
        // Fetch or create the content zone elements
        header: this.querySelector('coral-dialog-header') || document.createElement('coral-dialog-header'),
        content: this.querySelector('coral-dialog-content') || document.createElement('coral-dialog-content'),
        footer: this.querySelector('coral-dialog-footer') || document.createElement('coral-dialog-footer')
      });
      base.call(this._elements, {i18n});
  
      // Events
      this._delegateEvents({
        'coral-overlay:open': '_handleOpen',
        'click [coral-close]': '_handleCloseClick',
  
        // Since we cover the backdrop with ourself for positioning purposes, this is implemented as a click listener
        // instead of using backdropClickedCallback
        'click': '_handleClick',
  
        // Handle resize events
        'global:resize': 'center',
  
        'global:key:escape': '_handleEscape'
      });
  
      // Override defaults from Overlay
      this._trapFocus = this.constructor.trapFocus.ON;
      this._returnFocus = this.constructor.returnFocus.ON;
      this._overlayAnimationTime = this.constructor.FADETIME;
  
      // Listen for mutations
      this._headerObserver = new MutationObserver(this._hideHeaderIfEmpty.bind(this));
  
      // Watch for changes to the header element's children
      this._observeHeader();
    }
  
    /**
     Whether keyboard interaction is enabled. See {@link DialogInteractionEnum}.
  
     @type {DialogInteractionEnum}
     @default DialogInteractionEnum.ON
     */
    get interaction() {
      return this._interaction || interaction.ON;
    }
  
    set interaction(value) {
      value = transform.string(value).toLowerCase();
      this._interaction = validate.enumeration(interaction)(value) && value || interaction.ON;
    }
  
    /**
     The dialog header element.
  
     @type {DialogHeader}
     @contentzone
     */
    get header() {
      return this._getContentZone(this._elements.header);
    }
  
    set header(value) {
      this._setContentZone('header', value, {
        handle: 'header',
        tagName: 'coral-dialog-header',
        insert: function (header) {
          header.classList.add(`${CLASSNAME}-title`);
          // Position the header between the drag zone and the type icon
          this._elements.headerWrapper.insertBefore(header, this._elements.dragZone.nextElementSibling);
        },
        set: function () {
          // Stop observing the old header and observe the new one
          this._observeHeader();
  
          // Check if header needs to be hidden
          this._hideHeaderIfEmpty();
        }
      });
    }
  
    /**
     The dialog content element.
  
     @type {DialogContent}
     @contentzone
     */
    get content() {
      return this._getContentZone(this._elements.content);
    }
  
    set content(value) {
      this._setContentZone('content', value, {
        handle: 'content',
        tagName: 'coral-dialog-content',
        insert: function (content) {
          content.classList.add(`${CLASSNAME}-content`);
          const footer = this.footer;
          // The content should always be before footer
          this._elements.wrapper.insertBefore(content, this.contains(footer) && footer || null);
        }
      });
    }
  
    /**
     The dialog footer element.
  
     @type {DialogFooter}
     @contentzone
     */
    get footer() {
      return this._getContentZone(this._elements.footer);
    }
  
    set footer(value) {
      this._setContentZone('footer', value, {
        handle: 'footer',
        tagName: 'coral-dialog-footer',
        insert: function (footer) {
          footer.classList.add(`${CLASSNAME}-footer`);
          // The footer should always be after content
          this._elements.wrapper.appendChild(footer);
        }
      });
    }
  
    /**
     The backdrop configuration for this dialog. See {@link DialogBackdropEnum}.
  
     @type {String}
     @default DialogBackdropEnum.MODAL
     @htmlattribute backdrop
     */
    get backdrop() {
      return this._backdrop || backdrop.MODAL;
    }
  
    set backdrop(value) {
      value = transform.string(value).toLowerCase();
      this._backdrop = validate.enumeration(backdrop)(value) && value || backdrop.MODAL;
  
      const showBackdrop = this._backdrop !== backdrop.NONE;
      this._elements.wrapper.classList.toggle(`${CLASSNAME}--noBackdrop`, !showBackdrop);
  
      // We're visible now, so hide or show the modal accordingly
      if (this.open && showBackdrop) {
        this._showBackdrop();
      }
    }
  
    /**
     The dialog's variant. See {@link DialogVariantEnum}.
  
     @type {String}
     @default DialogVariantEnum.DEFAULT
     @htmlattribute variant
     @htmlattributereflected
     */
    get variant() {
      return this._variant || variant.DEFAULT;
    }
  
    set variant(value) {
      value = transform.string(value).toLowerCase();
      this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
      this._reflectAttribute('variant', this._variant);
  
      // Insert SVG icon
      this._insertTypeIcon();
  
      // Remove all variant classes
      this._elements.wrapper.classList.remove(...ALL_VARIANT_CLASSES);
  
      if (this._variant === variant.DEFAULT) {
        // ARIA
        this.setAttribute('role', 'dialog');
      } else {
        // Set new variant class
        this._elements.wrapper.classList.add(`${CLASSNAME}--${this._variant}`);
  
        // ARIA
        this.setAttribute('role', 'alertdialog');
      }
  
      const hasHeader = this.header && this.header.textContent !== '';
  
      // If the dialog has a header and is not otherwise labelled,
      if (hasHeader && !(this.hasAttribute('aria-labelledby') || this.hasAttribute('aria-label'))) {
        this.header.id = this.header.id || commons.getUID();
  
        // label the dialog with a reference to the header
        this.setAttribute('aria-labelledby', this.header.id);
      }
  
      // If the dialog has no content, or the content is empty, do nothing further.
      if (!this.content || this.content.textContent === '') {
        return;
      }
  
      // If the dialog has a content,
      this.content.id = this.content.id || commons.getUID();
  
      // In an alertdialog with a content region, if the alertdialog is not otherwise described.
      if (this._variant !== variant.DEFAULT) {
  
        // with no header,
        if (!hasHeader) {
  
          // label the alertdialog with a reference to the content
          this.setAttribute('aria-labelledby', this.content.id);
        }
  
        // otherwise, if the alertdialog is not otherwise described,
        else if (!this.hasAttribute('aria-describedby')) {
  
          // ensure that the alertdialog is described by the content.
          this.setAttribute('aria-describedby', this.content.id);
        }
      } else if (this.getAttribute('aria-labelledby') === this.content.id) {
        this.removeAttribute('aria-labelledby');
      }
    }
  
    /**
     Whether the dialog should be displayed full screen (without borders or margin).
  
     @type {Boolean}
     @default false
     @htmlattribute fullscreen
     @htmlattributereflected
     */
    get fullscreen() {
      return this._fullscreen || false;
    }
  
    set fullscreen(value) {
      this._fullscreen = transform.booleanAttr(value);
      this._reflectAttribute('fullscreen', this._fullscreen);
  
      if (this._fullscreen) {
        // Full screen and movable are not compatible
        this.movable = false;
        this._elements.wrapper.classList.add(FULLSCREEN_CLASSNAME);
      } else {
        this._elements.wrapper.classList.remove(FULLSCREEN_CLASSNAME);
      }
    }
  
    /**
     Inherited from {@link BaseOverlay#open}.
     */
    get open() {
      return super.open;
    }
  
    set open(value) {
      super.open = value;
  
      // Ensure we're in the DOM
      if (this.open) {
        // If not child of document.body, we have to move it there
        this._moveToDocumentBody();
  
        // Show the backdrop, if necessary
        if (this.backdrop !== backdrop.NONE) {
          this._showBackdrop();
        }
      }
  
      // Support animation
      requestAnimationFrame(() => {
        // Support wrapped dialog
        this._elements.wrapper.classList.toggle('is-open', this.open);
  
        // Handles what to focus based on focusOnShow
        if (this.open) {
          commons.transitionEnd(this._elements.wrapper, () => {
            this._handleFocus();
            this._elements.closeButton.tabIndex = 0;
            this._elements.closeButton.removeAttribute('coral-tabcapture');
          });
        } else {
          this._elements.closeButton.tabIndex = -1;
          this._elements.closeButton.setAttribute('coral-tabcapture', '');
        }
      });
    }
  
    /**
     The dialog's icon.
  
     @type {String}
     @default ""
     @htmlattribute icon
     */
    get icon() {
      return this._elements.icon;
    }
  
    set icon(value) {
      this._elements.icon = value;
    }
  
    /**
     Whether the dialog should have a close button. See {@link DialogClosableEnum}.
  
     @type {String}
     @default DialogClosableEnum.OFF
     @htmlattribute closable
     @htmlattributereflected
     */
    get closable() {
      return this._closable || closable.OFF;
    }
  
    set closable(value) {
      value = transform.string(value).toLowerCase();
      this._closable = validate.enumeration(closable)(value) && value || closable.OFF;
      this._reflectAttribute('closable', this._closable);
  
      this._elements.wrapper.classList.toggle(`${CLASSNAME}--dismissible`, this._closable === closable.ON);
    }
  
    /**
     Whether the dialog can moved around by dragging the title.
  
     @type {Boolean}
     @default false
     @htmlattribute movable
     @htmlattributereflected
     */
    get movable() {
      return this._movable || false;
    }
  
    set movable(value) {
      this._movable = transform.booleanAttr(value);
      this._reflectAttribute('movable', this._movable);
  
      // Movable and fullscreen are not compatible
      if (this._movable) {
        this.fullscreen = false;
      }
  
      if (this._movable) {
        const dragAction = new DragAction(this);
        dragAction.handle = this._elements.headerWrapper;
      } else {
        // Disables any dragging interaction
        if (this.dragAction) {
          this.dragAction.destroy();
        }
  
        // Recenter the dialog once it's not movable anymore
        this.center();
      }
    }
  
    /**
     Inherited from {@link BaseComponent#trackingElement}.
     */
    get trackingElement() {
      return typeof this._trackingElement === 'undefined' ?
        (this.header && this.header.textContent && this.header.textContent.replace(/\s{2,}/g, ' ').trim() || '') :
        this._trackingElement;
    }
  
    set trackingElement(value) {
      super.trackingElement = value;
    }
  
    /** @ignore */
    _observeHeader() {
      if (this._headerObserver) {
        this._headerObserver.disconnect();
  
        if (this._elements.header) {
          this._headerObserver.observe(this._elements.header, {
            // Catch changes to childList
            childList: true,
            // Catch changes to textContent
            characterData: true,
            // Monitor any child node
            subtree: true
          });
        }
      }
    }
  
    /**
     Hide the header wrapper if the header content zone is empty.
     @ignore
     */
    _hideHeaderIfEmpty() {
      const header = this._elements.header;
  
      if (header) {
        const headerWrapper = this._elements.headerWrapper;
  
        // If it's empty and has no non-textnode children, hide the header
        const hiddenValue = header.children.length === 0 && header.textContent.replace(/\s*/g, '') === '';
  
        // Only bother if the hidden status has changed
        if (hiddenValue !== headerWrapper.hidden) {
          headerWrapper.hidden = hiddenValue;
        }
  
        this.variant = this.variant;
      }
    }
  
    _handleOpen(event) {
      this._trackEvent('display', 'coral-dialog', event);
    }
  
    /** @ignore */
    _handleEscape(event) {
      // When escape is pressed, hide ourselves
      if (this.interaction === interaction.ON && this.open && this._isTopOverlay()) {
        event.stopPropagation();
        this.open = false;
      }
    }
  
    /**
     @ignore
     @todo maybe this should be base or something
     */
    _handleCloseClick(event) {
      const dismissTarget = event.matchedTarget;
      const dismissValue = dismissTarget.getAttribute('coral-close');
      if (!dismissValue || this.matches(dismissValue)) {
        this.open = false;
        event.stopPropagation();
  
        this._trackEvent('close', 'coral-dialog', event);
      }
    }
  
    _handleClick(event) {
      // When we're modal, we close when our outer area (over the backdrop) is clicked
      if (event.target === this && this.backdrop === backdrop.MODAL && this._isTopOverlay()) {
        this.open = false;
  
        this._trackEvent('close', 'coral-dialog', event);
      }
    }
  
    /** @ignore */
    _moveToDocumentBody() {
      // Not in the DOM
      if (!document.body.contains(this)) {
        document.body.appendChild(this);
      }
      // In the DOM but not a direct child of body
      else if (this.parentNode !== document.body) {
        this._ignoreConnectedCallback = true;
        this._repositioned = true;
        document.body.appendChild(this);
        this._ignoreConnectedCallback = false;
      }
    }
  
    _insertTypeIcon() {
      if (this._elements.icon) {
        this._elements.icon.remove();
      }
  
      let variantValue = this.variant;
  
      // Warning icon is same as ERROR icon
      if (variantValue === variant.WARNING || variantValue === variant.ERROR) {
        variantValue = 'alert';
      }
  
      // Inject the SVG icon
      if (variantValue !== variant.DEFAULT) {
        const iconName = capitalize(variantValue);
        this._elements.headerWrapper.insertAdjacentHTML('beforeend', Icon._renderSVG(`spectrum-css-icon-${iconName}Medium`, ['_coral-Dialog-typeIcon', `_coral-UIIcon-${iconName}Medium`]));
        this._elements.icon = this._elements.headerWrapper.querySelector('._coral-Dialog-typeIcon');
      }
    }
  
    /** @ignore */
    backdropClickedCallback() {
      // When we're modal, we close when the backdrop is clicked
      if (this.backdrop === backdrop.MODAL && this._isTopOverlay()) {
        this.open = false;
      }
    }
  
    /**
     Centers the dialog in the middle of the screen.
  
     @returns {Dialog} this, chainable.
     */
    center() {
      // We're already centered in fullscreen mode
      if (this.fullscreen) {
        return;
      }
  
      // If moved we reset the position
      this.style.top = '';
      this.style.left = '';
  
      return this;
    }
  
    get _contentZones() {
      return {
        'coral-dialog-header': 'header',
        'coral-dialog-content': 'content',
        'coral-dialog-footer': 'footer'
      };
    }
  
    /**
     Returns {@link Dialog} variants.
  
     @return {DialogVariantEnum}
     */
    static get variant() {
      return variant;
    }
  
    /**
     Returns {@link Dialog} backdrops.
  
     @return {DialogBackdropEnum}
     */
    static get backdrop() {
      return backdrop;
    }
  
    /**
     Returns {@link Dialog} close options.
  
     @return {DialogClosableEnum}
     */
    static get closable() {
      return closable;
    }
  
    /**
     Returns {@link Dialog} interaction options.
  
     @return {DialogInteractionEnum}
     */
    static get interaction() {
      return interaction;
    }
  
    /** @ignore */
    static get observedAttributes() {
      return super.observedAttributes.concat([
        'interaction',
        'backdrop',
        'variant',
        'fullscreen',
        'icon',
        'closable',
        'movable'
      ]);
    }
  
    /** @ignore */
    render() {
      super.render();
  
      this.classList.add(`${CLASSNAME}-wrapper`);
  
      // Default reflected attributes
      if (!this._variant) {
        this.variant = variant.DEFAULT;
      }
      if (!this._backdrop) {
        this.backdrop = backdrop.MODAL;
      }
      if (!this._closable) {
        this.closable = closable.OFF;
      }
      if (!this._interaction) {
        this.interaction = interaction.ON;
      }
  
      // Fetch the content zones
      const header = this._elements.header;
      const content = this._elements.content;
      const footer = this._elements.footer;
  
      // Verify if a content zone is provided
      const contentZoneProvided = this.contains(content) && content || this.contains(footer) && footer || this.contains(header) && header;
  
      // Verify if the internal wrapper exists
      let wrapper = this.querySelector(`.${CLASSNAME}`);
  
      // Case where the dialog was rendered already - cloneNode support
      if (wrapper) {
        // Remove tab captures
        Array.prototype.filter.call(this.children, (child) => child.hasAttribute('coral-tabcapture')).forEach((tabCapture) => {
          this.removeChild(tabCapture);
        });
  
        // Assign internal elements
        this._elements.headerWrapper = this.querySelector('._coral-Dialog-header');
        this._elements.closeButton = this.querySelector('._coral-Dialog-closeButton');
  
        this._elements.wrapper = wrapper;
      }
      // Case where the dialog needs to be rendered
      else {
        // Create default wrapper
        wrapper = this._elements.wrapper;
  
        // Create default header wrapper
        const headerWrapper = this._elements.headerWrapper;
  
        // Case where the dialog needs to be rendered and content zones are provided
        if (contentZoneProvided) {
          // Check if user wrapper is provided
          if (contentZoneProvided.parentNode === this) {
            // Content zone target defaults to default wrapper if no user wrapper element is provided
            this._elements.wrapper = wrapper;
          } else {
            // Content zone target defaults to user wrapper element if provided
            this._elements.wrapper = contentZoneProvided.parentNode;
          }
  
          // Move everything in the wrapper
          while (this.firstChild) {
            wrapper.appendChild(this.firstChild);
          }
  
          // Add the dialog header before the content
          this._elements.wrapper.insertBefore(headerWrapper, content);
        }
        // Case where the dialog needs to be rendered and content zones need to be created
        else {
          // Default content zone target is wrapper
          this._elements.wrapper = wrapper;
  
          // Move everything in the "content" content zone
          while (this.firstChild) {
            content.appendChild(this.firstChild);
          }
  
          // Add the content zones in the wrapper
          wrapper.appendChild(headerWrapper);
          wrapper.appendChild(content);
          wrapper.appendChild(footer);
        }
  
        // Add the wrapper to the dialog
        this.appendChild(wrapper);
      }
  
      // Only the wrapper gets the dialog class
      this._elements.wrapper.classList.add(CLASSNAME);
      // Mark the dialog with a public attribute for sizing
      this._elements.wrapper.setAttribute('coral-dialog-size', '');
      // Close button should stay under the dialog
      this._elements.wrapper.appendChild(this._elements.closeButton);
  
      // Copy styles over to new wrapper
      if (this._elements.wrapper.parentNode !== this) {
        const contentWrapper = this.querySelector('[handle="wrapper"]');
        Array.prototype.forEach.call(contentWrapper.classList, style => this._elements.wrapper.classList.add(style));
        contentWrapper.removeAttribute('class');
      }
  
      // Assign content zones
      this.header = header;
      this.footer = footer;
      this.content = content;
    }
  });
  
  export default Dialog;
  