popover/popover.vue

<template>
    <div class="ion-popover" :class="[modeClass,colorClass,cssClass]">
        <Backdrop :bdClick="bdClick" :enableBackdropDismiss="enableBackdropDismiss" :hidden="!showBackdrop"
                  :isActive="isActive"></Backdrop>
        <transition
                :name="popoverTransitionName"
                @before-enter="beforeEnter"
                @after-enter="afterEnter"
                @before-leave="beforeLeave"
                @after-leave="afterLeave">
            <div class="popover-wrapper" v-show="isActive">
                <div class="popover-arrow" ref="popoverArrow"></div>
                <div class="popover-content" ref="popoverContent">
                    <div class="popover-viewport" ref="viewport" v-html="htmlComponent"></div>
                    <component :is="theComponent" :data="data"></component>
                </div>
            </div>
        </transition>
    </div>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
  /**
   * @component Popover
   * @description
   *
   * ## 弹出层组件 / Popover提示框组件
   *
   * ### 简介
   *
   * 这个组件适用于对组件中某部分进行弹出提示, 比如:
   *
   * - 单词点击弹出翻译
   * - 点击按钮弹出可选择的操作(和Fab有点类似, 但是Popover可自定义程度高, 但是显示内容建议小于Modal组件)
   *
   * ### 传入模板的弹出层组件
   *
   * Popover的实现和Modal组件相似, 都需要传入`*.vue`模板文件, 具体事例参考usage
   *
   *
   * ### 子组件如何获取数据
   *
   * 在组件中使用: `this.$options.$data` 获取传入data. 例如Usage中的示例, 子组件获取data中的contentEle数据这样操作:
   *
   * ```
   * this.contentEle = this.$options.$data.contentEle
   * ```
   *
   *
   * @usage
   * import { Popover, List, ListHeader, ItemGroup, Item, ItemSliding, ItemOptions, ItemDivider } from 'vimo'
   * import TextTool from './textTool.vue'
   * export default{
   *  methods: {
   *    openSetting ($event) {
   *      Popover.present({
   *        ev: $event,                           // 事件
   *        component: TextTool,                  // 传入组件
   *        data: {
   *          contentEle: this.$refs.content.$el  // 传入数据, 内部通过`this.$options.$data`获取这个data
   *        }
   *      })
   *    },
   *    specialText ($event, text) {
   *      Popover.present({
   *        ev: $event,
   *        component: `<p style="padding:0 14px;" text-center>You choose the word of <strong>${text}</strong>.</p>`
   *      })
   *    }
   *  },
   *  components: {Popover, List, ListHeader, ItemGroup, Item, ItemSliding, ItemOptions, ItemDivider}
   * }
   *
   * @props {String} cssClass - 额外的样式
   * @props {mode} [mode='ios'] - 模式
   * @props {Boolean} [showBackdrop=true] - 是否显示backdrop
   * @props {Boolean} [enableBackdropDismiss=true] - 点击backdrop是否能关闭组件
   * @props {Boolean} [dismissOnPageChange=true] - 页面切换是否关闭组件, 默认关闭
   * @props {Function} [onDismiss] - 完全关闭时的回调
   *
   * @props {Object|String|Function|Promise} component - popover内部显示的vue组件, 是一个*.vue文件; 如果是String的话则为html字符串; 支持异步
   * @props {Object} data - 传给popover内部显示的vue组件的数据, 内部组件通过`this.$options.$data`获取
   * @props {Object|MouseEvent} ev - 点击元素的事件, $event, 这个值的传入可以计算popover放置的位置
   *
   * @demo #/popover
   * */
  import Backdrop from '../backdrop'
  import { parsePxUnit } from '../../util/util'
  import css from '../../util/get-css'

  const POPOVER_IOS_BODY_PADDING = 2
  const POPOVER_MD_BODY_PADDING = 12

  import popupExtend from '../../util/popup-extend'
  import prepareComponent from '../../util/prepare-component'

  export default {
    name: 'Popover',
    extends: popupExtend,
    data () {
      return {
        htmlComponent: '',
        theComponent: ''
      }
    },
    props: {
      component: [Object, String, Function, Promise],
      onDismiss: Function,
      data: Object,
      ev: [Object, MouseEvent], // 点击元素的事件
      mode: {
        type: String,
        default () { return this.$config && this.$config.get('mode', 'ios') || 'ios' }
      },
      cssClass: String,
      showBackdrop: {
        type: Boolean,
        default: true
      },
      enableBackdropDismiss: {
        type: Boolean,
        default: true
      }
    },
    computed: {
      modeClass () {
        return `popover popover-${this.mode}`
      },
      colorClass () {
        return this.color && `popover-${this.mode}-${this.color}`
      },
      popoverTransitionName () {
        return `popover-${this.mode}`
      },
      popoverEle () {
        return this.$refs.popoverContent
      },
      arrowEle () {
        return this.$refs.popoverArrow
      },
      viewportEle () {
        return this.$refs.viewport
      }
    },
    methods: {
      /**
       * 点击backdrop
       * @private
       * */
      bdClick () {
        if (this.enabled && this.enableBackdropDismiss) {
          this.dismiss()
        }
      },

      /**
       * 计算popover的位置/定位
       * @private
       * */
      mdPositionView (nativeEle, ev) {
        let originY = 'top'
        let originX = 'left'

        // Popover content width and height
        let popoverEle = this.popoverEle

        console.assert(popoverEle, 'The component Popover need popoverEle in mdPositionView()')

        let popoverWidth = parsePxUnit(window.getComputedStyle(popoverEle).width)
        let popoverHeight = parsePxUnit(window.getComputedStyle(popoverEle).height)

        // Window body width and height
        let bodyWidth = window['innerWidth']
        let bodyHeight = window['innerHeight']

        // If ev was passed, use that for target element
        let targetDim = this.getTargetDim(ev)
        let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2)
        let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2) - (popoverWidth / 2)
        let targetHeight = targetDim && targetDim.height || 0
        let popoverCSS = {
          top: targetTop,
          left: targetLeft
        }

        // If the popover left is less than the padding it is off screen
        // to the left so adjust it, else if the width of the popover
        // exceeds the body width it is off screen to the right so adjust
        if (popoverCSS.left < POPOVER_MD_BODY_PADDING) {
          popoverCSS.left = POPOVER_MD_BODY_PADDING
        } else if (popoverWidth + POPOVER_MD_BODY_PADDING + popoverCSS.left > bodyWidth) {
          popoverCSS.left = bodyWidth - popoverWidth - POPOVER_MD_BODY_PADDING
          originX = 'right'
        }

        // If the popover when popped down stretches past bottom of screen,
        // make it pop up if there's room above
        if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) {
          popoverCSS.top = targetTop - popoverHeight
          nativeEle.className = nativeEle.className + ' popover-bottom'
          originY = 'bottom'
          // If there isn't room for it to pop up above the target cut it off
        } else if (targetTop + targetHeight + popoverHeight > bodyHeight) {
          popoverEle.style.bottom = POPOVER_MD_BODY_PADDING + 'px'
        }

        popoverEle.style.top = popoverCSS.top + 'px'
        popoverEle.style.left = popoverCSS.left + 'px';
        (popoverEle.style)[css.transformOrigin] = originY + ' ' + originX
      },
      iosPositionView (nativeEle, ev) {
        let originY = 'top'
        let originX = 'left'
        // Popover content width and height
        let popoverEle = this.popoverEle
        let popoverWidth = parsePxUnit(window.getComputedStyle(popoverEle).width)
        let popoverHeight = parsePxUnit(window.getComputedStyle(popoverEle).height)

        // Window body width and height
        let bodyWidth = window['innerWidth']
        let bodyHeight = window['innerHeight']

        // If ev was passed, use that for target element
        let targetDim = this.getTargetDim(ev)
        let targetTop = (targetDim && 'top' in targetDim) ? targetDim.top : (bodyHeight / 2) - (popoverHeight / 2)
        let targetLeft = (targetDim && 'left' in targetDim) ? targetDim.left : (bodyWidth / 2)
        let targetWidth = targetDim && targetDim.width || 0
        let targetHeight = targetDim && targetDim.height || 0

        // The arrow that shows above the popover on iOS
        var arrowEle = this.arrowEle
        let arrowWidth = parsePxUnit(window.getComputedStyle(arrowEle).width)
        let arrowHeight = parsePxUnit(window.getComputedStyle(arrowEle).height)

        // If no ev was passed, hide the arrow
        if (!targetDim) {
          arrowEle.style.display = 'none'
        }
        let arrowCSS = {
          top: targetTop + targetHeight,
          left: targetLeft + (targetWidth / 2) - (arrowWidth / 2)
        }

        let popoverCSS = {
          top: targetTop + targetHeight + (arrowHeight - 1),
          left: targetLeft + (targetWidth / 2) - (popoverWidth / 2)
        }

        // If the popover left is less than the padding it is off screen
        // to the left so adjust it, else if the width of the popover
        // exceeds the body width it is off screen to the right so adjust
        if (popoverCSS.left < POPOVER_IOS_BODY_PADDING) {
          popoverCSS.left = POPOVER_IOS_BODY_PADDING
        } else if (popoverWidth + POPOVER_IOS_BODY_PADDING + popoverCSS.left > bodyWidth) {
          popoverCSS.left = bodyWidth - popoverWidth - POPOVER_IOS_BODY_PADDING
          originX = 'right'
        }

        // If the popover when popped down stretches past bottom of screen,
        // make it pop up if there's room above
        if (targetTop + targetHeight + popoverHeight > bodyHeight && targetTop - popoverHeight > 0) {
          arrowCSS.top = targetTop - (arrowHeight + 1)
          popoverCSS.top = targetTop - popoverHeight - (arrowHeight - 1)
          nativeEle.className = nativeEle.className + ' popover-bottom'
          originY = 'bottom'
          // If there isn't room for it to pop up above the target cut it off
        } else if (targetTop + targetHeight + popoverHeight > bodyHeight) {
          popoverEle.style.bottom = POPOVER_IOS_BODY_PADDING + '%'
        }

        arrowEle.style.top = arrowCSS.top + 'px'
        arrowEle.style.left = arrowCSS.left + 'px'

        popoverEle.style.top = popoverCSS.top + 'px'
        popoverEle.style.left = popoverCSS.left + 'px';
        (popoverEle.style)[css.transformOrigin] = originY + ' ' + originX
      },

      /**
       * 根据传入的event事件获取点击元素的尺寸
       * 如果没有事件则使用navbar中的站位元素,默认是在右上角
       * @private
       * */
      getTargetDim (ev) {
        if (ev && ev.target) {
          return ev.target.getBoundingClientRect()
        } else {
          let rightButtonPlaceholderElement = window.document.getElementById('right-button-placeholder')
          if (rightButtonPlaceholderElement) {
            return rightButtonPlaceholderElement.getBoundingClientRect()
          } else {
            return {}
          }
        }
      },

      beforeDismiss (data) {
        this.onDismiss && this.onDismiss(data)
      },

      /**
       * init
       * */
      init () {
        // 计算位置 渲染传入的组件
        if (this.mode === 'ios') {
          this.iosPositionView(this.$el, this.ev)
        } else {
          this.mdPositionView(this.$el, this.ev)
        }
      }
    },
    mounted () {
      prepareComponent(this.component).then((component) => {
        this.theComponent = component
        this.init()
      }, () => {
        // 如果不是 Component 则当做 html字符串处理
        this.htmlComponent = this.component
        this.init()
      })
    },
    components: {Backdrop}
  }
</script>