components/modal/modal.js

/**
 * @component Modal
 * @description
 *
 * ## 弹出层 / Modal弹出页组件
 *
 * ## 补充
 *
 * Model组件用于当前页面的补充, 相当于在当前页面上再来弹一个页面, 这里并不改变路由, 但是会改变H5 history的popstate, 每打开一个Modal则新增一个历史记录, 可以通过后退关闭最后一次打开的modal.
 *
 * ## 注意点
 *
 * - 在modal中如果跳转到另一页之前希望能先关闭当前modal再操作
 * - modal不会再url中留下记录
 * - modal只是装菜的盘子, 盘子中的菜通过`template`传入, 数据通过`modalData`传入,
 * - `onDismiss`会在modal关闭后触发.
 * - 开启的页面就是完整的Page页面, 别无其他
 *
 * ### 动画
 *
 * mode根据不同的设备启用不同的动画, 但是如果想自定义动画, 可以在mode中传入你自定义的结果. 另外, 默认自带zoom和fade两个动画.
 *
 * @usage
 * // 开启
 * openModal () {
 *        this.$modal.present({
 *          mode: 'zoom',
 *          component: modalPageComponent,
 *          data: {hello: 'Page1Data'},
 *          showBackdrop: true,
 *          enableBackdropDismiss: true,
 *          onDismiss (data) {
 *            console.debug('得到了modal1的关闭信息')
 *            console.debug(JSON.stringify(data))
 *          }
 *    })
 * },
 *
 * // 关闭
 * closeModal () {
 *        this.$modal.dismiss({
 *          result: 'modal 1 dismissed success!'
 *        })
 * }
 * @demo #/modal
 *
 * */

import Vue from 'vue'
import getInsertPosition from '../../util/getInsertPosition'
import { registerListener } from '../../util/util'
import modalComponent from './modal.vue'

let modalArr = []
let openIndex = 0
let unRegisterUrlChange = []
const Modal = Vue.extend(modalComponent)
let isModalEnable = true // 当modal处于打开过程和关闭过程中时为false, 其余状态为true
// ---------- functions ----------

/**
 * 获取实例
 * @private
 */
function ModalFactory (options) {
  return new Modal({
    el: getInsertPosition('modalPortal').appendChild(
      document.createElement('div')
    ),
    propsData: options
  })
}

/**
 * 开启Modal方法
 * 如果不懂想下: 桌子(页面)/菜盘(modal)/菜(template)的关系, 开启后获取Modal实例, 并将template初始化后挂在到Modal上, 然后注册urlChange事件. 在之后记录开启的Modal信息,
 * 然后执行modal实例的_present开启.
 *
 * @param {Object} options
 * @param {String} [options.mode]
 * @param {VueComponent} options.component - modal页面, 不支持异步
 * @param {object} [options.data] - 传给modal的数据
 * @param {function} [options.onDismiss] - 关闭model执行的操作, data是关闭时传入的参数
 * @param {Boolean} [options.showBackdrop=true] - 显示backdrop
 * @param {Boolean} [options.enableBackdropDismiss=true] - 点击backdrop是否关闭
 *
 *
 * @param {string} [options.mode='ios'] - 模式
 * @example
 * 传入参数示例:
 * {
 *  component:require('*.vue'),  // modal页面
 *  data:{...},            // 传给modal的数据
 *  onDismiss(data){....},      // 关闭model执行的操作, data是关闭时传入的参数
 * }
 *
 * 子页面通过 this.$options.$data.username 获取数据
 * */
function present (options = {}) {
  isModalEnable = false

  let modalInstance = ModalFactory(options)
  let presentPromise = modalInstance.present()

  // 增加浏览器历史记录
  window.history.pushState(
    {
      id: new Date().getTime(),
      name: options.name || `Modal-${openIndex++}`
    },
    '',
    ''
  )

  // record
  modalArr.push(modalInstance)

  // 如果是第一次进入则监听url变化
  if (unRegisterUrlChange.length === 0) {
    registerListener(
      window,
      'popstate',
      function () {
        if (isModalEnable) {
          // 总是关闭最后一次创建的modal
          let _lastModal = modalArr.pop()
          _lastModal && _lastModal.dismiss()
          // 如果是最后一个则解绑urlChange
          if (modalArr.length === 0) {
            unregisterAllListener()
            // 通知父页面更新navbar
            if (window.VM) {
              window.VM.$navbar &&
                window.VM.$navbar.initWhenInWebview &&
                window.VM.$navbar.initWhenInWebview()
              window.VM.$title &&
                window.VM.$title.init &&
                window.VM.$title.init()
            }
          } else {
            // 取出倒数第二个modal, 将nav更新为他的nav
            refreshNavbar(modalArr)
          }
        }
      },
      {},
      unRegisterUrlChange
    )
  }

  presentPromise.then(() => {
    isModalEnable = true
  })

  return presentPromise
}

/**
 * 全局注册dismiss方法
 * dismiss关闭最后一次打开的Modal, 并执行onDismiss函数, 就酱, 因为, modal是覆盖式的显示在页面上, 即使给定关闭的modal名字, 也无使用意义.
 * `dataBack`数据将由外部`onDismiss`接收
 * @param {*} dataBack -  modal调用dismiss传递向外的数据
 * */
function dismiss (dataBack) {
  isModalEnable = false

  return new Promise(resolve => {
    // 总是关闭最后一次创建的modal
    let lastModalInstance = modalArr.pop()
    // 如果是最后一个则解绑urlChange
    if (modalArr.length === 0) {
      unregisterAllListener()
      // 通知父页面更新navbar
      if (window.VM) {
        window.VM.$navbar &&
          window.VM.$navbar.initWhenInWebview &&
          window.VM.$navbar.initWhenInWebview()
        window.VM.$title && window.VM.$title.init && window.VM.$title.init()
      }
    } else {
      // 取出倒数第二个modal, 将nav更新为他的nav
      refreshNavbar(modalArr)
    }

    window.history.back()
    lastModalInstance.dismiss(dataBack).then(() => {
      isModalEnable = true
      // // 执行注册的onDismiss回调
      resolve()
    })
  })
}

function componentIsMatch (component, name) {
  return (
    component &&
    component.$options &&
    component.$options._componentTag.toString().toLowerCase() === name
  )
}

function refreshNavbar (modalArr) {
  let penultimateInstance = modalArr[modalArr.length - 1]
  let PageComponent = penultimateInstance.$children[1]
  if (PageComponent.$children && PageComponent.$children[0]) {
    if (componentIsMatch(PageComponent.$children[0], 'page')) {
      PageComponent = PageComponent.$children[0]
      let HeaderComponent = PageComponent.$children[0] // header content footer
      if (componentIsMatch(HeaderComponent, 'header')) {
        let NavbarComponent = HeaderComponent.$children[0]
        if (componentIsMatch(NavbarComponent, 'navbar')) {
          NavbarComponent.initWhenInWebview()
          for (
            let i = 0, len = NavbarComponent.$children.length;
            len > i;
            i++
          ) {
            if (componentIsMatch(NavbarComponent.$children[i], 'title')) {
              let TitleComponent = NavbarComponent.$children[i]
              TitleComponent.init()
              break
            }
          }
        }
      }
    }
  }
}

// 基础全部监听
function unregisterAllListener () {
  unRegisterUrlChange.forEach(item => item && item())
  unRegisterUrlChange = []
}

export default {
  present,
  dismiss
}