components/sheet/sheet.vue

<template>
    <div class="vm-sheet" :class="[modeClass,directionClass,{'vm-sheet-visible':isVisible}]">
        <vm-backdrop :bdClick="bdClick" :enableBackdropDismiss="enableBackdropDismiss"
                  :isActive="isActive && showBackdrop"></vm-backdrop>
        <transition
                name="sheet"
                @before-enter="beforeEnter"
                @after-enter="afterEnter"
                @before-leave="beforeLeave"
                @after-leave="afterLeave">
            <div class="sheet-wrapper" v-show="isActive" @touchmove="onTouchMoveHandler($event)">
                <div class="sheet-container">
                    <slot></slot>
                </div>
            </div>
        </transition>
    </div>
</template>
<script type="text/javascript">
  /**
   * @component Sheet
   * @description
   *
   * ## 弹出表单组件 / Sheet
   *
   * ### 介绍
   *
   * Sheet组件是一个模板组件, 自身只提供弹出载体, 其余业务部分通过slot传入, 且组件本身不对slot内容不作处理, 样式也由业务自己确定
   *
   * 组件开闭通过ref获取组件实例并通过下面两个方法操作:
   * - present() 开启
   * - dismiss() 关闭
   *
   *
   * ### 如何引入
   * ```
   * // 引入
   * import Sheet from 'vimo/lib/sheet'
   * // 安装
   * Vue.component(Sheet.name, Sheet)
   * // 或者
   * export default{
   *   src: {
   *     Sheet
   *  }
   * }
   * ```
   *
   * ### 关于slot
   *
   * 组件必须具有slot="fixed"属性, 表示固定在页面上. 如果没有的话, 页面滚动样式会产生错误.
   *
   * ### 如何开启
   *
   * 通过ref获取组件实例, 之后调用present方法开启, 调用dismiss方法关闭.
   *
   * ```
   * ...
   * computed: {
   *    paySheetCompoonent () {
   *      return this.$refs.paySheet
   *    }
   *  }
   * methods: {
   *    openPaySheet () {
   *      return this.paySheetCompoonent.present()
   *    },
   *    closePaySheet () {
   *      return this.paySheetCompoonent.dismiss()
   *    },
   *    choose (type) {
   *      this.closePaySheet().then(() => {
   *        this.type = type
   *      })
   *    }
   *  }
   * ...
   * ```
   *
   * @props {String} [direction='button'] - 表单出现位置, 只能是: 'bottom', 'top' 两个方向
   * @props {Boolean} [enableBackdropDismiss=true] - 点击背景关闭组件
   * @props {String} [mode='ios'] - 模式
   * @props {Boolean} [dismissOnPageChange=true] - 页面切换关闭组件
   * @props {boolean} [showBackdrop=true] - 是否显示黑色背景
   *
   * @demo #/sheet
   * @usage
   * <vm-sheet ref="paySheet" slot="fixed" direction="button">
   *    <section class="pay-sheet">
   *    <div class="pay-sheet-title">选择支付方式</div>
   *    <div class="pay-sheet-container">
   *        <div class="pay-sheet-content" @click="choose('alipay')">
   *            <vm-icon class="pay-sheet-icon" name="icon-alipay">Icon</vm-icon>
   *            <p class="pay-sheet-name">支付宝</p>
   *        </div>
   *        <div class="pay-sheet-content" @click="choose('wechat')">
   *            <vm-icon class="pay-sheet-icon" name="icon-weichat">Icon</vm-icon>
   *            <p class="pay-sheet-name">微信</p>
   *        </div>
   *        <div class="pay-sheet-content" @click="choose('unipay')">
   *            <vm-icon class="pay-sheet-icon" name="icon-unipay">Icon</vm-icon>
   *            <p class="pay-sheet-name">银联</p>
   *        </div>
   *    </div>
   *    <div class="pay-sheet-buttons">
   *        <vm-button full clear class="pay-sheet-button" @click="closePaySheet">取消</vm-button>
   *    </div>
   *    </section>
   * </vm-sheet>
   *
   * */
  import Backdrop from '../backdrop'
  import { urlChange } from '../../util/util'
  import ThemeMixins from '../../themes/theme.mixins'

  const NOOP = () => {}

  export default {
    name: 'vm-sheet',
    mixins: [ThemeMixins],
    props: {
      direction: {
        type: String,
        default: 'bottom',
        validator (val) {
          return ['bottom', 'top'].indexOf(val) > -1
        }
      },
      enableBackdropDismiss: {
        type: Boolean,
        default: true
      },
      dismissOnPageChange: {
        type: Boolean,
        default: true
      },
      showBackdrop: {
        type: Boolean,
        default: true
      }
    },
    data () {
      return {
        /**
         * @desc
         * ActionSheet State
         * 属性      | 描述             | 未开启 | 'Present动画'开始 | 'Present动画'结束 | 'Dismiss动画'开始 | 'Dismiss动画'结束 | 未开启
         * ----------|------------------|--------|-------------------|-------------------|-------------------|-------------------|--------
         * isActive  | 开启关闭状态     | false  | true              | true              | true              | false             | false
         * enabled   | 是否在过渡动画中 | true   | false             | true              | true              | false             | true
         * isVisible | 判断是否在可视区 | false  | true              | true              | true              | true              | false
         * @private
         * */
        isActive: false,    // Sheet 开启关闭状态, 显示动画开启 -> 触发关闭dismiss
        isVisible: false,    // 是否处在显示阶段, 显示动画开启 -> 关闭动画完全关闭
        enabled: true,     // 是否在过渡态的状态判断,如果在动画中则为false (补漏的)

        // promise
        presentCallback: NOOP,
        dismissCallback: NOOP,

        unReg: null         // 页面变化的解绑函数
      }
    },
    computed: {
      directionClass () {
        return `sheet-direction-${this.direction}`
      }
    },
    methods: {
      /**
       * ActionSheet Animate Hooks
       * @private
       * */
      beforeEnter () {
        this.enabled = false // 不允许过渡中途操作
        this.isVisible = true
        this.$app && this.$app.setEnabled(false, 400)
      },
      afterEnter () {
        this.presentCallback()
        this.focusOutActiveElement()
        let focusableEle = document.querySelector('button')
        if (focusableEle) {
          focusableEle.focus()
        }
        this.enabled = true
        this.isVisible = true
      },
      beforeLeave () {
        this.enabled = false
        this.isVisible = true
        this.$app && this.$app.setEnabled(false, 400)
      },
      afterLeave () {
        this.dismissCallback()
        this.enabled = true
        this.isVisible = false
      },

      /**
       * ActionSheet启动之前去除focus效果,因为存在键盘
       * @private
       * */
      focusOutActiveElement () {
        // only button?
        const activeElement = document.activeElement
        activeElement && activeElement.blur && activeElement.blur()
      },

      /**
       * @function bdClick
       * @description
       * 点击backdrop,关闭actionsheet
       * @private
       */
      bdClick () {
        if (this.enableBackdropDismiss) {
          this.dismiss()
        }
      },

      /**
       * @function click
       * @description
       * 点击下方按钮
       * @param {object} button Button Click Handler
       * @private
       */
      click (button) {
        let shouldDismiss = true
        if (button.handler) {
          // a handler has been provided, execute it
          if (button.handler() === false) {
            // if the return value of the handler is false then do not dismiss
            shouldDismiss = false
          }
        }

        // 当前不是在过渡动画中(dismissing中),
        // 如果是在dismissing中,则意味着正在关闭,
        // 这里不必进行
        if (shouldDismiss) {
          this.dismiss()
        }
      },

      /**
       * @function present
       * @description
       * 打开ActionSheet
       * @returns {Promise}  结果返回Promise, 当动画完毕后执行resolved
       */
      present () {
        this.isActive = true
        return new Promise((resolve) => { this.presentCallback = resolve })
      },

      /**
       * @function dismiss
       * @description
       * 关闭ActionSheet, 如果组件还在动画中, 则执行dismiss没有动作
       * @return {Promise} 结果返回Promise, 当动画完毕后执行resolved
       * */
      dismiss () {
        if (!this.enabled) {
          return new Promise((resolve) => { resolve() })
        }
        if (this.isActive) {
          this.isActive = false // 动起来
          this.unReg && this.unReg()
          return new Promise((resolve) => { this.dismissCallback = resolve })
        } else {
          return new Promise((resolve) => { resolve() })
        }
      },

      onTouchMoveHandler ($event) {
        $event.preventDefault()
        $event.stopPropagation()
      }
    },
    created () {
      // mounted before data ready, so no need to judge the `dismissOnPageChange` value
      if (this.dismissOnPageChange) {
        this.unReg = urlChange(() => {
          this.isActive && this.dismiss()
        })
      }
    },
    components: {
      Backdrop
    }
  }
</script>
<style lang="scss">
    @import "sheet";
    @import "sheet.ios";
    @import "sheet.md";
</style>