<template>
  <div ref="popupRef">
    <transition :name="animationName" :duration="duration"
                @after-enter="handleAfterEnter"
                @after-leave="handleAfterLeave">
      <div class="el-trigger el-popper"
           :class="[popperClass, content && 'el-trigger--plain', 'el-trigger-position-' + position]"
           ref="popper"
           :trigger-placement="position"
           v-show="!disabled && showPopper"
           role="tooltip"
           :id="tooltipId"
           :aria-hidden="(disabled || !showPopper) ? 'true' : 'false'">
        <div :class="['el-trigger-content', contentClass]" :style="contentStyle" ref="slotData">
          <slot name="content"></slot>
        </div>
        <div :class="['trigger__arrow', arrowClass]" :style="arrowStyleData" v-if="showArrow"></div>
      </div>
    </transition>
    <div class="el-trigger__reference-wrapper" ref="wrapper">
      <slot name="reference"></slot>
    </div>
  </div>
</template>

<script>
import Popper from 'modesign-ui/src/utils/vue-popper';
import {on, off} from 'modesign-ui/src/utils/dom';
import {addClass, removeClass} from 'modesign-ui/src/utils/dom';
import {generateId} from 'modesign-ui/src/utils/util';

export default {
  name: "ElTrigger",
  data() {
    return {
      transformStyle: '',
      customStyle: '',
      arrowStyleData: {},
      popupPosition: {},
      scrollElements: [],
      mousePosition: {
        top: 0,
        left: 0
      },
      delayTimer: 0
    }
  },
  mixins: [Popper],
  mounted() {
    this.popupPosition = Object.assign({}, this.position)
    this.currentPlacement = this.position
    let reference = this.referenceElm = this.reference || this.$refs.reference;
    const popper = this.$refs.popper;
    if (!reference && this.$refs.wrapper.children) {
      reference = this.referenceElm = this.$refs.wrapper.children[0];
    }
    // 可访问性
    if (reference) {
      addClass(reference, 'el-trigger__reference');
      reference.setAttribute('aria-describedby', this.tooltipId);
      reference.setAttribute('tabindex', this.tabindex); // tab序列
      popper.setAttribute('tabindex', 0);
      if (!this.trigger.includes('click')) {
        on(reference, 'focusin', () => {
          this.handleFocus();
          const instance = reference.__vue__;
          if (instance && typeof instance.focus === 'function') {
            instance.focus();
          }
        });
        on(popper, 'focusin', this.handleFocus);
        on(reference, 'focusout', this.handleBlur);
        on(popper, 'focusout', this.handleBlur);
      }
      on(reference, 'mousemove', this.handleMouseMove)
      on(reference, 'keydown', this.handleKeydown);
      on(reference, 'click', this.handleClick);
    }
    if (this.trigger.includes('click')) {
      on(reference, 'click', this.doToggle);
      on(reference, 'mouseenter', this.doToggleEnter)
      on(reference, 'mouseleave', this.doToggleLeave)
      on(reference, 'contextmenu', this.doToggleContextMenu)
      on(document, 'click', this.handleDocumentClick);
    }
    if (this.trigger.includes('hover')) {
      on(reference, 'mouseenter', this.handleMouseEnter);
      on(popper, 'mouseenter', this.handleMouseEnter);
      on(reference, 'mouseleave', this.handleMouseLeave);
      on(popper, 'mouseleave', this.handleMouseLeave);
      on(reference, 'mousemove', this.handleMouseMove)
    }
    if (this.trigger.includes('focus')) {
      if (this.tabindex < 0) {
        console.warn('[MOdesign Warn][ElTrigger]a negative taindex means that the element cannot be focused by tab key');
      }
      if (reference.querySelector('input, textarea')) {
        on(reference, 'focusin', this.doShow);
        on(reference, 'focusout', this.doClose);
      } else {
        on(reference, 'mousedown', this.doShow);
        on(reference, 'mouseup', this.doClose);
      }
    }
    if (this.updateAtScroll) {
      this.scrollElements = this.getScrollElements(this.$refs.popupRef)
      for(const item of this.scrollElements) {
        item.addEventListener('scroll', this.handleScroll)
      }
    }
    document.oncontextmenu = function(){
      return false;
    }
  },
  methods: {
    doToggleContextMenu(e) {
      if (this.disabled || !this.trigger.includes('contextMenu') || (this.showPopper && !this.clickToClose)) return
      this.updateMousePosition(e)
      this.showPopper = true
      this.updatePopper()
      e.preventDefault()
    },
    handleMouseMove(e) {
      if (this.disabled || !this.trigger.includes('hover')) return
      this.updateMousePosition(e)
      this.showPopper = true
      this.updatePopper({left: 5, top: 5})
    },
    doToggleEnter(e) {
      if (this.disabled || !this.trigger.includes('hover')) return
      this.updateMousePosition(e)
      this.showPopper = true
      this.updatePopper()
    },
    doToggleLeave() {
      if (this.disabled || !this.trigger.includes('hover')) return
      this.showPopper = false
    },
    doToggle(e) {
      if (this.trigger.includes('hover') || this.trigger.includes('focus')) return
      if (this.disabled || (this.showPopper && !this.clickToClose)) return
      this.updateMousePosition(e)
      if (this.trigger.includes('click')) {
        this.showPopper = !this.showPopper;
        if (this.showPopper) {
          this.updatePopupStyle()
        }
      } else if (this.trigger.includes('contextMenu') && this.showPopper) {
        this.showPopper = false
      }
    },
    handleScroll(e) {
      if (this.showPopper) {
        this.updatePopper()
      }
    },
    doShow(e) {
      this.showPopper = true;
    },
    doClose() {
      this.showPopper = false;
    },
    handleFocus() {
      addClass(this.referenceElm, 'focusing');
      if (this.trigger.includes('click') || this.trigger.includes('focus')) this.showPopper = true;
    },
    handleClick() {
      removeClass(this.referenceElm, 'focusing');
    },
    handleBlur() {
      removeClass(this.referenceElm, 'focusing');
      if (this.trigger.includes('click') || this.trigger.includes('focus')) this.showPopper = false;
    },
    getElementScrollRect(element, containerRect) {
      const rect = element.getBoundingClientRect();
      return {
        top: rect.top,
        bottom: rect.bottom,
        left: rect.left,
        right: rect.right,
        scrollTop: rect.top - containerRect.top,
        scrollBottom: rect.bottom - containerRect.top,
        scrollLeft: rect.left - containerRect.left,
        scrollRight: rect.right - containerRect.left,
        width: element.offsetWidth ? element.clientWidth : 0,
        height: element.offsetHeight ? element.clientHeight : 0,
      };
    },
    /** 更新弹框位置 **/
    updatePopupStyle() {
      const containerRect = document.body.getBoundingClientRect()
      let triggerRect
      if (this.alignPoint) {
        triggerRect = {
          top: this.mousePosition.top,
          bottom: this.mousePosition.top,
          left: this.mousePosition.left,
          right: this.mousePosition.left,
          scrollTop: this.mousePosition.top,
          scrollBottom: this.mousePosition.top,
          scrollLeft: this.mousePosition.left,
          scrollRight: this.mousePosition.left,
          width: 0,
          height: 0,
        }
      } else {
        triggerRect = this.getElementScrollRect(this.$refs.reference || this.referenceElm, containerRect)
      }
      const _this = this

      function getPopupRect() {
        return _this.getElementScrollRect(_this.$refs.popupRef, containerRect)
      }

      if (this.autoFixTransformOrigin) {
        this.transformStyle = {
          transformOrigin: this.getTransformOrigin(this.position)
        }
      }

      if (this.popupPosition !== this.position) {
        this.popupPosition = this.position;
      }
      if (this.showArrow) {
        this.$nextTick(() => {
          this.arrowStyleData = this.getArrowStyle(
              this.position,
              triggerRect,
              _this.getElementScrollRect(_this.$refs.popper, containerRect),
              getPopupRect(),
              {
                customStyle: this.arrowStyle,
              }
          );
        });
      }
    },
    isScrollElement(element) {
      return (
          element.scrollHeight > element.offsetHeight ||
              element.scrollWidth > element.offsetWidth
      )
    },
    getScrollElements(scrollRect) {
      const scrollElements = []
      let element = scrollRect
      while (element && element !== document.documentElement) {
        if (this.isScrollElement(element)) {
          scrollElements.push(element)
        }
        element = element.parentElement || undefined
      }
      return scrollElements
    },
    getArrowStyle(position, triggerRect, popupRect, container, style) {
      if (['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end'].includes(position)) {
        let offsetLeft = Math.abs(
            triggerRect.scrollLeft + triggerRect.width / 2 - popupRect.scrollLeft
        );
        if (offsetLeft > popupRect.width - 12) {
          if (triggerRect.width > popupRect.width) {
            offsetLeft = popupRect.width / 2;
          } else {
            offsetLeft = popupRect.width - 12;
          }
        }

        if (['top', 'top-start', 'top-end'].includes(position)) {
          return {
            left: `${offsetLeft}px`,
            bottom: '0',
            transform: 'translate(-50%,50%) rotate(45deg)',
            ...style.customStyle,
          };
        }
        return {
          left: `${offsetLeft}px`,
          top: '0',
          transform: 'translate(-50%,-50%) rotate(45deg)',
          ...style.customStyle,
        };
      }
      let offsetTop
      if (['right-start', 'left-start'].includes(position)) {
        offsetTop = Math.abs(
            triggerRect.scrollTop + container.height / 2 - container.scrollTop
        );
      } else if (['right', 'left'].includes(position)) {
        offsetTop = Math.abs(popupRect.height / 2);
      } else {
        offsetTop = Math.abs(
            triggerRect.scrollTop + popupRect.height / 2 - popupRect.scrollTop
        );
      }

      if (offsetTop > popupRect.height - 12) {
        if (triggerRect.height > popupRect.height) {
          offsetTop = popupRect.height / 2;
        } else {
          offsetTop = popupRect.height - 12;
        }
      }

      if (['left', 'left-start', 'left-end'].includes(position)) {
        return {
          top: `${offsetTop}px`,
          right: '0',
          transform: 'translate(50%,-50%) rotate(45deg)',
          ...style.customStyle,
        };
      }
      return {
        top: `${offsetTop}px`,
        left: '0',
        transform: 'translate(-50%,-50%) rotate(45deg)',
        ...style.customStyle,
      };
    },
    getTransformOrigin(position) {
      let originX = '0';
      if (['top', 'bottom'].includes(position)) {
        originX = '50%';
      } else if (['left', 'left-start', 'left-end', 'top-end', 'bottom-end'].includes(position)) {
        originX = '100%';
      }
      let originY = '0';
      if (['left', 'right'].includes(position)) {
        originY = '50%';
      } else if (['top', 'top-start', 'top-end', 'left-start', 'right-end'].includes(position)) {
        originY = '100%';
      }
      return `${originX} ${originY}`;
    },
    handleMouseEnter(e) {
      if (this.disabled || !this.trigger.includes('hover')) return;
      this.updateMousePosition(e)
    },
    handleKeydown(ev) {
      if (ev.keyCode === 27 && this.trigger !== 'manual') { // esc
        this.doClose();
      }
    },
    handleMouseLeave(e) {
      if (this.disabled || !this.trigger.includes('hover')) return;
      if (!this.popupHoverStay || e.target.role === 'tooltip') {
        this.showPopper = false
      }
    },
    handleDocumentClick(e) {
      let reference = this.reference || this.$refs.reference;
      const popper = this.$refs.popper;

      if (!reference && this.$refs.wrapper.children) {
        reference = this.referenceElm = this.$refs.wrapper.children[0];
      }
      if (!this.$el ||
          !reference ||
          this.$el.contains(e.target) ||
          reference.contains(e.target) ||
          !popper ||
          popper.contains(e.target)) return;
      this.updateMousePosition(e)
      this.showPopper = false;
    },
    updateMousePosition(e) {
      if (this.alignPoint) {
        const { pageX, pageY } = e
        this.mousePosition = {
          top: pageY,
          left: pageX
        }
        this.positionData = this.mousePosition
      }
    },
    handleAfterEnter(e) {
      this.$emit('show', true);
      this.$emit('popup-visible-change', true)
    },
    handleAfterLeave() {
      this.$emit('hide', true);
      this.$emit('popup-visible-change', false)
      this.doDestroy();
    }
  },
  beforeDestroy() {
    document.oncontextmenu = function(){
      return true;
    }
  },
  destroyed() {
    const reference = this.reference;
    off(reference, 'click', this.doToggle);
    off(reference, 'mouseup', this.doClose);
    off(reference, 'mousedown', this.doShow);
    off(reference, 'focusin', this.doShow);
    off(reference, 'focusout', this.doClose);
    off(reference, 'mousedown', this.doShow);
    off(reference, 'mouseup', this.doClose);
    off(reference, 'mousemove', this.handleMouseMove)
    off(reference, 'mouseleave', this.handleMouseLeave);
    off(reference, 'mouseenter', this.handleMouseEnter);
    off(document, 'click', this.handleDocumentClick);
  },
  computed: {
    tooltipId() {
      return `el-trigger-${generateId()}`;
    }
  },
  props: {
    popperClass: String,
    content: String,
    /**
     * 触发方式 'hover' | 'click' | 'focus' | 'contextMenu'
     */
    trigger: {
      type: [String, Array],
      default: 'hover'
    },
    /**
     * 弹出位置 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'
     */
    position: {
      type: String,
      default: 'bottom'
    },
    /**
     * 触发器是否禁用
     */
    disabled: {
      type: Boolean,
      default: false
    },
    /** 弹出框是否显示箭头 **/
    showArrow: {
      type: Boolean,
      default: false
    },
    /** 弹出框是否跟随鼠标 **/
    alignPoint: {
      type: Boolean,
      default: false
    },
    /** 是否在移出触发器，并移入弹出框时保持弹出框显示 **/
    popupHoverStay: {
      type: Boolean,
      default: true
    },
    /** 是否在点击触发器时关闭弹出框 **/
    clickToClose: {
      type: Boolean,
      default: true
    },
    /** 弹出框内容的类名 **/
    contentClass: {
      type: [String, Array, Object]
    },
    /** 弹出框内容的样式 **/
    contentStyle: {
      type: Object
    },
    /** 弹出框箭头的类名 **/
    arrowClass: {
      type: [String, Array, Object]
    },
    /** 弹出框箭头的样式 **/
    arrowStyle: {
      type: Object
    },
    /** 弹出动画的name **/
    animationName: {
      type: String,
      default: 'fade-in'
    },
    /** 弹出动画的持续时间 (number| { enter: number; leave: number; })**/
    duration: {
      type: [Number, Object]
    },
    /** 是否在容器滚动时更新弹出框的位置 **/
    updateAtScroll: {
      type: Boolean,
      default: false
    },
    autoFixTransformOrigin: {
      type: Boolean,
      default: false
    },
    tabindex: {
      type: Number,
      default: 0
    }
  }
}
</script>

<style scoped>

</style>
