Blame view

node_modules/bootstrap-vue/src/components/modal/helpers/bv-modal.js 9 KB
4cd4fd28   郭伟龙   feat: 初始化项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// Plugin for adding `$bvModal` property to all Vue instances
import { NAME_MODAL, NAME_MSG_BOX } from '../../../constants/components'
import {
  EVENT_NAME_HIDDEN,
  EVENT_NAME_HIDE,
  HOOK_EVENT_NAME_BEFORE_DESTROY,
  HOOK_EVENT_NAME_DESTROYED
} from '../../../constants/events'
import { useParentMixin } from '../../../mixins/use-parent'
import { concat } from '../../../utils/array'
import { getComponentConfig } from '../../../utils/config'
import { requestAF } from '../../../utils/dom'
import { getRootActionEventName } from '../../../utils/events'
import { isUndefined, isFunction } from '../../../utils/inspect'
import {
  assign,
  defineProperties,
  defineProperty,
  hasOwnProperty,
  keys,
  omit,
  readonlyDescriptor
} from '../../../utils/object'
import { pluginFactory } from '../../../utils/plugins'
import { warn, warnNotClient, warnNoPromiseSupport } from '../../../utils/warn'
import { createNewChildComponent } from '../../../utils/create-new-child-component'
import { getEventRoot } from '../../../utils/get-event-root'
import { BModal, props as modalProps } from '../modal'

// --- Constants ---

const PROP_NAME = '$bvModal'
const PROP_NAME_PRIV = '_bv__modal'

// Base modal props that are allowed
// Some may be ignored or overridden on some message boxes
// Prop ID is allowed, but really only should be used for testing
// We need to add it in explicitly as it comes from the `idMixin`
const BASE_PROPS = [
  'id',
  ...keys(omit(modalProps, ['busy', 'lazy', 'noStacking', 'static', 'visible']))
]

// Fallback event resolver (returns undefined)
const defaultResolver = () => {}

// Map prop names to modal slot names
const propsToSlots = {
  msgBoxContent: 'default',
  title: 'modal-title',
  okTitle: 'modal-ok',
  cancelTitle: 'modal-cancel'
}

// --- Helper methods ---

// Method to filter only recognized props that are not undefined
const filterOptions = options => {
  return BASE_PROPS.reduce((memo, key) => {
    if (!isUndefined(options[key])) {
      memo[key] = options[key]
    }
    return memo
  }, {})
}

// Method to install `$bvModal` VM injection
const plugin = Vue => {
  // Create a private sub-component that extends BModal
  // which self-destructs after hidden
  // @vue/component
  const BMsgBox = Vue.extend({
    name: NAME_MSG_BOX,
    extends: BModal,
    mixins: [useParentMixin],
    destroyed() {
      // Make sure we not in document any more
      if (this.$el && this.$el.parentNode) {
        this.$el.parentNode.removeChild(this.$el)
      }
    },
    mounted() {
      // Self destruct handler
      const handleDestroy = () => {
        this.$nextTick(() => {
          // In a `requestAF()` to release control back to application
          requestAF(() => {
            this.$destroy()
          })
        })
      }
      // Self destruct if parent destroyed
      this.bvParent.$once(HOOK_EVENT_NAME_DESTROYED, handleDestroy)
      // Self destruct after hidden
      this.$once(EVENT_NAME_HIDDEN, handleDestroy)
      // Self destruct on route change
      /* istanbul ignore if */
      if (this.$router && this.$route) {
        // Destroy ourselves if route changes
        /* istanbul ignore next */
        this.$once(HOOK_EVENT_NAME_BEFORE_DESTROY, this.$watch('$router', handleDestroy))
      }
      // Show the `BMsgBox`
      this.show()
    }
  })

  // Method to generate the on-demand modal message box
  // Returns a promise that resolves to a value returned by the resolve
  const asyncMsgBox = (parent, props, resolver = defaultResolver) => {
    if (warnNotClient(PROP_NAME) || warnNoPromiseSupport(PROP_NAME)) {
      /* istanbul ignore next */
      return
    }
    // Create an instance of `BMsgBox` component
    // We set parent as the local VM so these modals can emit events on
    // the app `$root`, as needed by things like tooltips and popovers
    // And it helps to ensure `BMsgBox` is destroyed when parent is destroyed
    const msgBox = createNewChildComponent(parent, BMsgBox, {
      // Preset the prop values
      propsData: {
        ...filterOptions(getComponentConfig(NAME_MODAL)),
        // Defaults that user can override
        hideHeaderClose: true,
        hideHeader: !(props.title || props.titleHtml),
        // Add in (filtered) user supplied props
        ...omit(props, keys(propsToSlots)),
        // Props that can't be overridden
        lazy: false,
        busy: false,
        visible: false,
        noStacking: false,
        noEnforceFocus: false
      }
    })
    // Convert certain props to scoped slots
    keys(propsToSlots).forEach(prop => {
      if (!isUndefined(props[prop])) {
        // Can be a string, or array of VNodes.
        // Alternatively, user can use HTML version of prop to pass an HTML string.
        msgBox.$slots[propsToSlots[prop]] = concat(props[prop])
      }
    })
    // Return a promise that resolves when hidden, or rejects on destroyed
    return new Promise((resolve, reject) => {
      let resolved = false
      msgBox.$once(HOOK_EVENT_NAME_DESTROYED, () => {
        if (!resolved) {
          /* istanbul ignore next */
          reject(new Error('BootstrapVue MsgBox destroyed before resolve'))
        }
      })
      msgBox.$on(EVENT_NAME_HIDE, bvModalEvent => {
        if (!bvModalEvent.defaultPrevented) {
          const result = resolver(bvModalEvent)
          // If resolver didn't cancel hide, we resolve
          if (!bvModalEvent.defaultPrevented) {
            resolved = true
            resolve(result)
          }
        }
      })
      // Create a mount point (a DIV) and mount the msgBo which will trigger it to show
      const div = document.createElement('div')
      document.body.appendChild(div)
      msgBox.$mount(div)
    })
  }

  // Private utility method to open a user defined message box and returns a promise.
  // Not to be used directly by consumers, as this method may change calling syntax
  const makeMsgBox = (parent, content, options = {}, resolver = null) => {
    if (
      !content ||
      warnNoPromiseSupport(PROP_NAME) ||
      warnNotClient(PROP_NAME) ||
      !isFunction(resolver)
    ) {
      /* istanbul ignore next */
      return
    }
    return asyncMsgBox(parent, { ...filterOptions(options), msgBoxContent: content }, resolver)
  }

  // BvModal instance class
  class BvModal {
    constructor(vm) {
      // Assign the new properties to this instance
      assign(this, { _vm: vm, _root: getEventRoot(vm) })
      // Set these properties as read-only and non-enumerable
      defineProperties(this, {
        _vm: readonlyDescriptor(),
        _root: readonlyDescriptor()
      })
    }

    // --- Instance methods ---

    // Show modal with the specified ID args are for future use
    show(id, ...args) {
      if (id && this._root) {
        this._root.$emit(getRootActionEventName(NAME_MODAL, 'show'), id, ...args)
      }
    }

    // Hide modal with the specified ID args are for future use
    hide(id, ...args) {
      if (id && this._root) {
        this._root.$emit(getRootActionEventName(NAME_MODAL, 'hide'), id, ...args)
      }
    }

    // The following methods require Promise support!
    // IE 11 and others do not support Promise natively, so users
    // should have a Polyfill loaded (which they need anyways for IE 11 support)

    // Open a message box with OK button only and returns a promise
    msgBoxOk(message, options = {}) {
      // Pick the modal props we support from options
      const props = {
        ...options,
        // Add in overrides and our content prop
        okOnly: true,
        okDisabled: false,
        hideFooter: false,
        msgBoxContent: message
      }
      return makeMsgBox(this._vm, message, props, () => {
        // Always resolve to true for OK
        return true
      })
    }

    // Open a message box modal with OK and CANCEL buttons
    // and returns a promise
    msgBoxConfirm(message, options = {}) {
      // Set the modal props we support from options
      const props = {
        ...options,
        // Add in overrides and our content prop
        okOnly: false,
        okDisabled: false,
        cancelDisabled: false,
        hideFooter: false
      }
      return makeMsgBox(this._vm, message, props, bvModalEvent => {
        const trigger = bvModalEvent.trigger
        return trigger === 'ok' ? true : trigger === 'cancel' ? false : null
      })
    }
  }

  // Add our instance mixin
  Vue.mixin({
    beforeCreate() {
      // Because we need access to `$root` for `$emits`, and VM for parenting,
      // we have to create a fresh instance of `BvModal` for each VM
      this[PROP_NAME_PRIV] = new BvModal(this)
    }
  })

  // Define our read-only `$bvModal` instance property
  // Placed in an if just in case in HMR mode
  if (!hasOwnProperty(Vue.prototype, PROP_NAME)) {
    defineProperty(Vue.prototype, PROP_NAME, {
      get() {
        /* istanbul ignore next */
        if (!this || !this[PROP_NAME_PRIV]) {
          warn(`"${PROP_NAME}" must be accessed from a Vue instance "this" context.`, NAME_MODAL)
        }
        return this[PROP_NAME_PRIV]
      }
    })
  }
}

export const BVModalPlugin = /*#__PURE__*/ pluginFactory({
  plugins: { plugin }
})