/**
 * 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 Vent from '@adobe/vent';
import {events, validate, transform, commons} from '../../../coral-utils';

// Attributes
const DROP_ZONE_ATTRIBUTE = 'coral-dragaction-dropzone';
const HANDLE_ATTRIBUTE = 'coral-dragaction-handle';
const AXIS_ATTRIBUTE = 'coral-dragaction-axis';
const SCROLL_ATTRIBUTE = 'coral-dragaction-scroll';
const CONTAINMENT_ATTRIBUTE = 'coral-dragaction-containment';

// Classes
const OPEN_HAND_CLASS = 'u-coral-openHand';
const CLOSE_HAND_CLASS = 'u-coral-closedHand';
const IS_DRAGGING_CLASS = 'is-dragging';

// Scroll offset default values
const DEFAULT_SCROLL_OFFSET = 20;
const DEFAULT_SCROLL_BY = 10;

/**
 Enumeration for {@link DragAction} axis restrictions.

 @typedef {Object} DragActionAxisEnum

 @property {String} FREE
 Allows vertically and horizontally dragging.
 @property {String} VERTICAL
 Allows vertically dragging only.
 @property {String} HORIZONTAL
 Allows horizontally dragging only.
 */
const axis = {
  FREE: 'free',
  VERTICAL: 'vertical',
  HORIZONTAL: 'horizontal'
};

/**
 @ignore
 @param {HTMLElement} element
 @returns {HTMLElement}
 First parent element with overflow [hidden|scroll|auto]
 */
function getViewContainer(element) {
  while (element) {
    const p = element.parentNode;

    if (!p) {
      return p;
    }
    if (p.matches('body')) {
      return p;
    }

    const computedStyle = window.getComputedStyle(p);
    const overflow = computedStyle.overflow;

    // IE11 can return a value for overflow even if it was not set compared to other browsers so we check for X and Y.
    const overflowX = computedStyle.overflowX;
    const overflowY = computedStyle.overflowY;

    if ((overflow === 'hidden' || overflow === 'auto' || overflow === 'scroll') &&
      // @polyfill IE11
      overflow === overflowX && overflow === overflowY) {
      return p;
    }

    element = p;
  }
}

/**
 @ignore
 @param {String|HTMLElement|NodeList} el
 @returns {Array.<HTMLElement>}
 X and y position whether event was generated by a click or a touch
 */
function transformToArray(el) {
  if (typeof el === 'string') {
    return Array.prototype.slice.call(document.querySelectorAll(el));
  } else if (el instanceof HTMLElement) {
    return [el];
  } else if (Object.prototype.toString.call(el) === '[object NodeList]') {
    return Array.prototype.slice.call(el);
  }
}

/**
 @ignore
 @param {Object} event
 @returns {Object}
 X and y position whether event was generated by a click or a touch
 */
function getPagePosition(event) {
  let touch = {};

  if (event.changedTouches && event.changedTouches.length > 0) {
    touch = event.changedTouches[0];
  } else if (event.touches && event.touches.length > 0) {
    touch = event.touches[0];
  }

  return {
    x: touch.pageX || event.pageX,
    y: touch.pageY || event.pageY
  };
}

/**
 @ignore
 @param {HTMLElement} scrollingElement
 element that scrolls the document
 @param {HTMLElement} a
 @param {HTMLElement} b
 @returns {Boolean}
 Whether a is within b bounds
 */
function within(scrollingElement, a, b) {
  const aBoundingClientRect = a.getBoundingClientRect();
  const bBoundingClientRect = b.getBoundingClientRect();
  const documentScrollTop = scrollingElement.scrollTop;
  const documentScrollLeft = scrollingElement.scrollLeft;

  const al = aBoundingClientRect.left + documentScrollLeft;
  const ar = al + aBoundingClientRect.width;
  const bl = bBoundingClientRect.left + documentScrollLeft;
  const br = bl + bBoundingClientRect.width;

  const at = aBoundingClientRect.top + documentScrollTop;
  const ab = at + aBoundingClientRect.height;
  const bt = bBoundingClientRect.top + documentScrollTop;
  const bb = bt + bBoundingClientRect.height;

  return !(bl > ar || br < al || (bt > ab || bb < at));
}

/**
 @ignore
 @param {DragAction} dragAction
 Coral.DragAction instance
 @returns {HTMLElement}
 The dropzone that is being hovered by the dragged element or null if none
 */
function isOverDropZone(dragAction) {
  let el = null;
  if (dragAction._dropZones && dragAction._dropZones.length) {
    dragAction._dropZones.some((dropZone) => {
      if (within(dragAction._scrollingElement, dragAction._dragElement, dropZone)) {
        el = dropZone;
        return true;
      }

      return false;
    });
  }

  return el;
}

function getScrollParent(node) {
  if (!node) {
    return null;
  }

  return node.scrollHeight > node.clientHeight ? node : getScrollParent(node.parentNode);
}

/**
 @class Coral.DragAction
 @classdesc This a decorator which adds draggable functionality to elements.
 To define draggable actions on specific elements, handles can be used.
 A handle is given a special attribute :
 - <code>coral-dragaction</code> attribute adds draggable functionality to the corresponding element.
 - <code>coral-dragaction-handle</code> attribute allows dragging only by dragging the specified handle.
 - <code>coral-dragaction-dropzone</code> attribute is used to indicate possible dropzones making it possible
 to build drag-and-drop enabled interfaces in conjunction with <code>DragAction</code> events.
 - <code>coral-dragaction-axis</code> and setting it to either <code>horizontal</code> or <code>vertical</code>,
 it is possible to restrict the drag'n'drop to a single axis.
 - <code>coral-dragaction-scroll</code> attribute will scroll the container when the draggable is moved beyond the viewport.
 - <code>coral-dragaction-containment</code>, the draggable element will be constrained to its container.
 @param {String|HTMLElement} dragElement
 The draggable element.
 */
class DragAction {
  /**
   Takes the {HTMLElement} to be dragged as argument.

   @param {HTMLElement} dragElement
   */
  constructor(dragElement) {
    if (!dragElement) {
      throw new Error('Coral.DragAction: dragElement is missing');
    }

    let el = null;
    if (dragElement instanceof HTMLElement) {
      el = dragElement;
    } else if (typeof dragElement === 'string') {
      el = document.querySelector(dragElement);
      if (!el) {
        throw new Error('Coral.DragAction: dragElement is null');
      }
    }

    this._id = commons.getUID();
    this._dragElementValue = dragElement;
    this._dragElement = el;

    // Destroy instance if existing
    if (this._dragElement.dragAction) {
      this._dragElement.dragAction.destroy();
    }

    const computedStyle = window.getComputedStyle(this._dragElement);

    // Store initial position
    this._initialPosition = {
      position: computedStyle.position,
      left: computedStyle.left,
      top: computedStyle.top
    };

    // Prepare Vent
    this._dragEvents = new Vent(this._dragElement);

    // Handle options. Binds events to dragElement if no handles defined or found
    this.handle = this._dragElement.getAttribute(HANDLE_ATTRIBUTE);

    // DropZone options
    this.dropZone = this._dragElement.getAttribute(DROP_ZONE_ATTRIBUTE);

    // Axis horizontal|vertical
    this.axis = this._dragElement.getAttribute(AXIS_ATTRIBUTE);

    // Scroll options
    this.scroll = this._dragElement.matches(`[${SCROLL_ATTRIBUTE}]`);

    // Restriction to container
    this.containment = this._dragElement.matches(`[${CONTAINMENT_ATTRIBUTE}]`);

    this._drag = this._drag.bind(this);
    this._dragEnd = this._dragEnd.bind(this);
    this._dragOnKeyEnd = this._dragOnKeyEnd.bind(this);

    events.on(`touchmove.DragAction${this._id}`, this._drag);
    events.on(`mousemove.DragAction${this._id}`, this._drag);
    events.on(`touchend.DragAction${this._id}`, this._dragEnd);
    events.on(`mouseup.DragAction${this._id}`, this._dragEnd);

    // Store reference on dragElement
    this._dragElement.dragAction = this;
  }

  /**
   The draggable element.

   @name dragElement
   @readonly
   @type {String|HTMLElement}
   @htmlattribute coral-dragaction
   */
  get dragElement() {
    return this._dragElementValue;
  }

  /**
   The handle allowing to drag the element.

   @name handle
   @type {String|HTMLElement}
   @htmlattribute coral-dragaction-handle
   */
  get handle() {
    return this._handle;
  }

  set handle(value) {
    // Set new value
    this._handle = value;

    // Unbind events
    this._dragEvents.off('.DragAction');

    // Remove classes
    document.body.classList.remove(CLOSE_HAND_CLASS);
    this._dragElement.classList.remove(IS_DRAGGING_CLASS);
    if (this._handles && this._handles.length) {
      this._handles.forEach((handle) => {
        handle._dragEvents.off('.DragAction');
        handle.classList.remove(OPEN_HAND_CLASS);
      });
    } else {
      this._dragElement.classList.remove(OPEN_HAND_CLASS);
    }

    if (typeof value === 'string' ||
      value instanceof HTMLElement ||
      Object.prototype.toString.call(value) === '[object NodeList]') {
      this._handles = transformToArray(value);

      // Bind events
      if (this._handles && this._handles.length) {
        this._handles.forEach((handle) => {
          handle._dragEvents = handle._dragEvents || new Vent(handle);
          handle._dragEvents.on('mousedown.DragAction', this._dragStart.bind(this));
          handle._dragEvents.on('touchstart.DragAction', this._dragStart.bind(this));
          handle._dragEvents.on('keydown.DragAction', this._dragOnKeyDown.bind(this));
          handle._dragEvents.on('keyup.DragAction', this._dragOnKeyUp.bind(this));
          handle._dragEvents.on('focusout.DragAction', this._dragOnFocusOut.bind(this));
          handle.classList.add(OPEN_HAND_CLASS);
        });
      } else {
        this._dragEvents.on('touchstart.DragAction', this._dragStart.bind(this));
        this._dragEvents.on('mousedown.DragAction', this._dragStart.bind(this));
        this._dragEvents.on('keydown.DragAction', this._dragOnKeyDown.bind(this));
        this._dragEvents.on('keyup.DragAction', this._dragOnKeyUp.bind(this));
        this._dragEvents.on('focusout.DragAction', this._dragOnFocusOut.bind(this));
        this._dragElement.classList.add(OPEN_HAND_CLASS);
      }
    } else {
      // Defaults to the dragElement
      this._handles = [];
      this._dragEvents.on('touchstart.DragAction', this._dragStart.bind(this));
      this._dragEvents.on('mousedown.DragAction', this._dragStart.bind(this));
      this._dragEvents.on('keydown.DragAction', this._dragOnKeyDown.bind(this));
      this._dragEvents.on('keyup.DragAction', this._dragOnKeyUp.bind(this));
      this._dragEvents.on('focusout.DragAction', this._dragOnFocusOut.bind(this));
      this._dragElement.classList.add(OPEN_HAND_CLASS);
    }
  }

  /**
   The dropZone to drop the dragged element.

   @name dropZone
   @type {String|HTMLElement}
   @htmlattribute coral-dragaction-dropzone
   */
  get dropZone() {
    return this._dropZone;
  }

  set dropZone(value) {
    // Set new value
    this._dropZone = value;
    this._dropZoneEntered = false;

    if (typeof value === 'string' ||
      value instanceof HTMLElement ||
      Object.prototype.toString.call(value) === '[object NodeList]') {
      this._dropZones = transformToArray(value);
    } else {
      this._dropZones = [];
    }
  }

  /**
   The axis to constrain drag movement. See {@link DragActionAxisEnum}.

   @name axis
   @type {String}
   @default DragActionAxisEnum.FREE
   @htmlattribute coral-dragaction-axis
   */
  get axis() {
    return this._axis;
  }

  set axis(value) {
    value = transform.string(value);
    this._axis = validate.enumeration(axis)(value) && value || axis.FREE;
  }

  /**
   Whether to scroll the container when the draggable element is moved beyond the viewport.

   @name scroll
   @default false
   @type {Boolean}
   @htmlattribute coral-dragaction-scroll
   */
  get scroll() {
    return this._scroll;
  }

  set scroll(value) {
    this._scroll = transform.boolean(value);
  }

  get useScrollParent() {
    return this._useScrollParent || false;
  }

  set useScrollParent(value) {
    this._useScrollParent = transform.boolean(value);
  }

  /**
   Whether to constrain the draggable element to its container viewport.

   @name containment
   @default false
   @type {Boolean}
   @htmlattribute coral-dragaction-containment
   */
  get containment() {
    return this._containment;
  }

  set containment(value) {
    this._containment = transform.boolean(value);
  }

  /** @private */
  _dragStart(event) {
    if (events.isVirtualEvent(event)) {
      return;
    }

    // Container
    const viewContainer = getViewContainer(this._dragElement) || document.body;
    this._container = this.useScrollParent ? getScrollParent(this._dragElement) || viewContainer : viewContainer;

    // Prevent dragging ghost image
    if (event.target.tagName === 'IMG') {
      event.preventDefault();
    }

    // Prevent touchscreen windows to scroll while dragging
    events.on('touchmove.DragAction', (e) => {
      e.preventDefault();
    });

    document.body._overflow = window.getComputedStyle(document.body).overflow;
    document.body.style.overflow = 'hidden';

    if (!this._container.matches('body')) {
      this._container._overflow = window.getComputedStyle(this._container).overflow;
      this._container.style.overflow = this.scroll ? 'scroll' : 'hidden';
    }

    const pagePosition = getPagePosition(event);
    const dragElementBoundingClientRect = this._dragElement.getBoundingClientRect();
    this._dragPosition = getPagePosition(event);
    this._dragPosition.y -= dragElementBoundingClientRect.top + this._scrollingElement.scrollTop;
    this._dragPosition.x -= dragElementBoundingClientRect.left + this._scrollingElement.scrollLeft;

    // Handle classes
    document.body.classList.add(CLOSE_HAND_CLASS);
    if (this._handles && this._handles.length) {
      this._handles.forEach((handle) => {
        handle.classList.remove(OPEN_HAND_CLASS);
      });
    } else {
      this._dragElement.classList.remove(OPEN_HAND_CLASS);
    }
    this._dragElement.classList.add(IS_DRAGGING_CLASS);

    // Apply relative position by default
    if (window.getComputedStyle(this._dragElement).position === 'static') {
      this._dragElement.style.position = 'relative';
    }

    this._dragEvents.dispatch('coral-dragaction:dragstart', {
      detail: {
        dragElement: this._dragElement,
        pageX: pagePosition.x,
        pageY: pagePosition.y
      }
    });
  }

  /** @private */
  _drag(event) {
    if (this._dragElement.classList.contains(IS_DRAGGING_CLASS)) {
      const pagePosition = getPagePosition(event);

      const documentScrollTop = this._scrollingElement.scrollTop;
      const documentScrollLeft = this._scrollingElement.scrollLeft;

      const dragElementBoundingClientRect = this._dragElement.getBoundingClientRect();
      const dragElementHeight = dragElementBoundingClientRect.height;
      const dragElementWidth = dragElementBoundingClientRect.width;
      const dragElementPosition = {
        top: dragElementBoundingClientRect.top + documentScrollTop,
        left: dragElementBoundingClientRect.left + documentScrollLeft
      };
      const dragElementComputedStyle = window.getComputedStyle(this._dragElement);
      const dragElementCSSPosition = {
        top: parseFloat(dragElementComputedStyle.top) || 0,
        left: parseFloat(dragElementComputedStyle.left) || 0
      };

      const containerBoundingClientRect = this._container.getBoundingClientRect();
      const containerWidth = containerBoundingClientRect.width;
      const containerHeight = containerBoundingClientRect.height;
      const containerPosition = {
        top: containerBoundingClientRect.top + documentScrollTop,
        left: containerBoundingClientRect.left + documentScrollLeft
      };

      this._dragEvents.dispatch('coral-dragaction:drag', {
        detail: {
          dragElement: this._dragElement,
          pageX: pagePosition.x,
          pageY: pagePosition.y
        }
      });

      // Remove selection
      if (document.selection) {
        document.selection.empty();
      } else if (window.getSelection) {
        // @polyfill ie
        if (window.getSelection().removeAllRanges) {
          window.getSelection().removeAllRanges();
        }
      }

      // Need to scroll ?
      if (this.scroll) {
        // Scroll element is the document
        if (this._container === document.body) {
          // Scroll to the top
          if (dragElementBoundingClientRect.top < DEFAULT_SCROLL_OFFSET) {
            this._scrollingElement.scrollTop = documentScrollTop - DEFAULT_SCROLL_BY;
          }
          // Scroll to the bottom but don't go further than the maximum scroll position of the document
          else if (dragElementBoundingClientRect.top + dragElementBoundingClientRect.height > window.innerHeight - DEFAULT_SCROLL_OFFSET &&
            dragElementPosition.top + dragElementBoundingClientRect.height + DEFAULT_SCROLL_OFFSET < this._scrollingElement.scrollHeight) {
            this._scrollingElement.scrollTop = documentScrollTop + DEFAULT_SCROLL_BY;
          }

          // Scroll to the left
          if (dragElementBoundingClientRect.left < DEFAULT_SCROLL_OFFSET) {
            this._scrollingElement.scrollLeft = documentScrollLeft - DEFAULT_SCROLL_BY;
          }
          // Scroll to the right but don't go further than the maximum scroll position of the document
          else if (dragElementBoundingClientRect.left + dragElementBoundingClientRect.width > window.innerWidth - DEFAULT_SCROLL_OFFSET &&
            dragElementPosition.left + dragElementBoundingClientRect.width + DEFAULT_SCROLL_OFFSET < this._scrollingElement.scrollWidth) {
            this._scrollingElement.scrollLeft = documentScrollLeft + DEFAULT_SCROLL_BY;
          }
        }
        // Scroll element is an element other than the document
        else {
          // Scroll to the top
          if (dragElementBoundingClientRect.top - containerBoundingClientRect.top < DEFAULT_SCROLL_OFFSET) {
            this._container.scrollTop = this._container.scrollTop - DEFAULT_SCROLL_BY;
          }
          // Scroll to the bottom but don't go further than the maximum scroll position of the container
          else if (dragElementBoundingClientRect.top - containerBoundingClientRect.top + dragElementBoundingClientRect.height > containerBoundingClientRect.height - DEFAULT_SCROLL_OFFSET &&
            dragElementBoundingClientRect.top - containerBoundingClientRect.top + dragElementBoundingClientRect.height < containerBoundingClientRect.height) {
            this._container.scrollTop = this._container.scrollTop + DEFAULT_SCROLL_BY;
          }

          // Scroll to the left
          if (dragElementBoundingClientRect.left - containerBoundingClientRect.left < DEFAULT_SCROLL_OFFSET) {
            this._container.scrollLeft = this._container.scrollLeft - DEFAULT_SCROLL_BY;
          }
          // Scroll to the bottom but don't go further than the maximum scroll position of the container
          else if (dragElementBoundingClientRect.left - containerBoundingClientRect.left + dragElementBoundingClientRect.width > containerBoundingClientRect.width - DEFAULT_SCROLL_OFFSET &&
            dragElementBoundingClientRect.left - containerBoundingClientRect.left + dragElementBoundingClientRect.width < containerBoundingClientRect.width) {
            this._container.scrollLeft = this._container.scrollLeft + DEFAULT_SCROLL_BY;
          }
        }
      }

      // Set drag element's new position
      const newPosition = {};

      if (this.axis !== 'horizontal') {
        const top = pagePosition.y - this._dragPosition.y;

        // Applying container containment for y movements
        if (this.containment) {
          if (top >= containerPosition.top && top + dragElementHeight <= containerPosition.top + containerHeight) {
            newPosition.top = top;
          }
          // put the drag element to the container's top
          else if (pagePosition.y <= containerPosition.top) {
            newPosition.top = containerPosition.top;
          }
          // put the drag element to the container's bottom
          else if (pagePosition.y >= containerPosition.top + containerHeight) {
            newPosition.top = containerPosition.top + containerHeight - dragElementHeight;
          }
        } else {
          newPosition.top = top;
        }
      }
      if (this.axis !== 'vertical') {
        const left = pagePosition.x - this._dragPosition.x;

        // Applying container containment for x movements
        if (this.containment) {
          if (left >= containerPosition.left && left + dragElementWidth <= containerPosition.left + containerWidth) {
            newPosition.left = left;
          }
          // put the drag element to the container's left
          else if (pagePosition.x <= containerPosition.left) {
            newPosition.left = containerPosition.left;
          }
          // put the drag element to the container's right
          else if (pagePosition.x >= containerPosition.left + containerWidth) {
            newPosition.left = containerPosition.left + containerWidth - dragElementWidth;
          }
        } else {
          newPosition.left = left;
        }
      }

      // Set the new position
      this._dragElement.style.top = `${newPosition.top - dragElementPosition.top + dragElementCSSPosition.top}px`;
      this._dragElement.style.left = `${newPosition.left - dragElementPosition.left + dragElementCSSPosition.left}px`;

      // Trigger dropzone related events
      const dropZone = isOverDropZone(this);
      if (dropZone) {
        this._dropElement = dropZone;
        if (!this._dropZoneEntered) {
          this._dropZoneEntered = true;
          this._dragEvents.dispatch('coral-dragaction:dragenter', {
            detail: {
              dragElement: this._dragElement,
              pageX: pagePosition.x,
              pageY: pagePosition.y,
              dropElement: this._dropElement
            }
          });
        }

        this._dragEvents.dispatch('coral-dragaction:dragover', {
          detail: {
            dragElement: this._dragElement,
            pageX: pagePosition.x,
            pageY: pagePosition.y,
            dropElement: this._dropElement
          }
        });
      } else if (this._dropZoneEntered) {
        this._dragEvents.dispatch('coral-dragaction:dragleave', {
          detail: {
            dragElement: this._dragElement,
            pageX: pagePosition.x,
            pageY: pagePosition.y,
            dropElement: this._dropElement
          }
        });
        this._dropZoneEntered = false;
      }
    }
  }

  /** @private */
  _dragEnd(event) {
    if (this._dragElement.classList.contains(IS_DRAGGING_CLASS)) {
      const pagePosition = getPagePosition(event);

      // Restore overflow
      document.body.style.overflow = document.body._overflow;
      document.body._overflow = undefined;

      if (!this._container.matches('body')) {
        this._container.style.overflow = this._container._overflow;
        this._container._overflow = undefined;
      }

      document.body.classList.remove(CLOSE_HAND_CLASS);
      this._dragElement.classList.remove(IS_DRAGGING_CLASS);

      if (this._handles && this._handles.length) {
        this._handles.forEach((handle) => {
          handle.classList.add(OPEN_HAND_CLASS);
        });
      } else {
        this._dragElement.classList.add(OPEN_HAND_CLASS);
      }

      if (this._dropZoneEntered) {
        const dropZone = isOverDropZone(this);
        if (dropZone) {
          this._dropElement = dropZone;
          this._dragEvents.dispatch('coral-dragaction:drop', {
            detail: {
              dragElement: this._dragElement,
              pageX: pagePosition.x,
              pageY: pagePosition.y,
              dropElement: this._dropElement
            }
          });
        }
      }

      this._dragEvents.dispatch('coral-dragaction:dragend', {
        detail: {
          dragElement: this._dragElement,
          pageX: pagePosition.x,
          pageY: pagePosition.y
        }
      });
    }
  }

  /** @private */
  _dragOnKeyDown(event) {
    switch (event.code) {
      case 'Space':
      case 'ArrowDown':
      case 'ArrowUp':
      case 'Enter':
      case 'Escape':
        event.preventDefault();
        break;
    }
  }

  /** @private */
  _dragOnKeyUp(event) {
    switch (event.code) {
      case 'Space':
        if (!this.isKeyboardDragging) {
          this._dragEvents.dispatch('coral-dragaction:dragonkeyspace', {
            detail: {
              dragElement: this._dragElement
            }
          });
          this.isKeyboardDragging = true;
        } else {
          this._dragOnKeyEnd();
        }
        break;
      case 'ArrowDown':
        this._dragEvents.dispatch('coral-dragaction:dragoveronkeyarrowdown', {
          detail: {
            dragElement: this._dragElement
          }
        });
        break;
      case 'ArrowUp':
        this._dragEvents.dispatch('coral-dragaction:dragoveronkeyarrowup', {
          detail: {
            dragElement: this._dragElement
          }
        });
        break;
      case 'Enter':
      case 'Escape':
        this._dragOnKeyEnd();
        break;
    }
  }

  /** @private */
  _dragOnFocusOut(event) {
    window.setTimeout(() => {
      if (document.activeElement === event.target) {
        return;
      }
      this._dragOnKeyEnd();
    }, 0);
  }

  /** @private */
  _dragOnKeyEnd() {
    this._dragEvents.dispatch('coral-dragaction:dragendonkey', {
      detail: {
        dragElement: this._dragElement
      }
    });
    this.isKeyboardDragging = false;
  }

  /**
   Remove draggable actions

   @function destroy
   @param {Boolean} restorePosition
   Whether to restore the draggable element to its initial position
   */
  destroy(restorePosition) {
    // Unbind events and remove classes
    document.body.classList.remove(CLOSE_HAND_CLASS);
    this._dragElement.classList.remove(IS_DRAGGING_CLASS);
    if (this._handles && this._handles.length) {
      this._handles.forEach((handle) => {
        handle._dragEvents.off('.DragAction');
        handle.classList.remove(OPEN_HAND_CLASS);
      });
    } else {
      this._dragEvents.off('.DragAction');
      this._dragElement.classList.remove(OPEN_HAND_CLASS);
    }

    events.off(`.DragAction${this._id}`);

    // Restore overflow
    if (document.body._overflow) {
      document.body.style.overflow = document.body._overflow;
      document.body._overflow = undefined;
    }

    // Container might not have been initialized
    if (this._container) {
      if (!this._container.matches('body') && this._container._overflow) {
        this._container.style.overflow = this._container._overflow;
        this._container._overflow = undefined;
      }
    }

    // Set to initial position
    if (restorePosition) {
      this._dragElement.style.position = this._initialPosition.position;
      this._dragElement.style.top = this._initialPosition.top;
      this._dragElement.style.left = this._initialPosition.left;
    }

    // Remove reference
    this._dragElement.dragAction = undefined;
  }

  /**
   Returns {@link DragAction} axis restrictions.

   @return {DragActionAxisEnum}
   */
  static get axis() {
    return axis;
  }

  /** @private */
  get _scrollingElement() {
    // @polyfill ie11
    // Element that scrolls the document.
    return document.scrollingElement || document.documentElement;
  }

  /**
   Triggered when the {@link DragAction#dragElement} starts to be dragged.

   @typedef {CustomEvent} coral-dragaction:dragstart

   @property {HTMLElement} dragElement
   The dragged element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} is being dragged.

   @typedef {CustomEvent} coral-dragaction:drag

   @property {HTMLElement} dragElement
   The dragged element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} stops to be dragged.

   @typedef {CustomEvent} coral-dragaction:dragend

   @property {HTMLElement} dragElement
   The dragged element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} enters a drop element.

   @typedef {CustomEvent} coral-dragaction:dragenter

   @property {HTMLElement} dragElement
   The dragged element
   @property {HTMLElement} dropElement
   The drop element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} is over a drop element.

   @typedef {CustomEvent} coral-dragaction:dragover

   @property {HTMLElement} dragElement
   The dragged element
   @property {HTMLElement} dropElement
   The drop element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} leaves a drop element.

   @typedef {CustomEvent} coral-dragaction:dragleave

   @property {HTMLElement} dragElement
   The dragged element
   @property {HTMLElement} dropElement
   The drop element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} is dropped on a drop element.

   @typedef {CustomEvent} coral-dragaction:drop

   @property {HTMLElement} dragElement
   The dragged element
   @property {HTMLElement} dropElement
   The drop element
   @property {Number} pageX
   The mouse position relative to the left edge of the document.
   @property {Number} pageY
   The mouse position relative to the top edge of the document.
   */

  /**
   Triggered when the {@link DragAction#dragElement} is selected to be dragged.
   @typedef {CustomEvent} coral-dragaction:dragonkeyspace
   @property {HTMLElement} dragElement
   The dragged element
   */

  /**
   Triggered when the {@link DragAction#dragElement} is moved on arrow down pressed.
   @typedef {CustomEvent} coral-dragaction:dragoveronkeyarrowdown
   @property {HTMLElement} dragElement
   The dragged element
   */

  /**
   Triggered when the {@link DragAction#dragElement} is moved on arrow up pressed.
   @typedef {CustomEvent} coral-dragaction:dragoveronkeyarrowup
   @property {HTMLElement} dragElement
   The dragged element
   */

  /**
   Triggered when the {@link DragAction#dragElement} is losing focus.
   @typedef {CustomEvent} coral-dragaction:dragonkeyfocusout
   @property {HTMLElement} dragElement
   The dragged element
   */

  /**
   Triggered when the {@link DragAction#dragElement} is dropped on key.
   @typedef {CustomEvent} coral-dragaction:dragendonkey
   @property {HTMLElement} dragElement
   The dragged element
   */
}

export default DragAction;
