From e1d4993a4ab8c178e672d1e7af15e948eec06503 Mon Sep 17 00:00:00 2001
From: DaxChen
Date: Fri, 6 Oct 2017 16:23:10 +0800
Subject: [PATCH] fixes #60, #15: detect renderedHeight when "auto"; call
"opened" after DOM changes
---
src/Modal.vue | 123 ++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 104 insertions(+), 19 deletions(-)
diff --git a/src/Modal.vue b/src/Modal.vue
index 44c25a2..ae9bc13 100644
--- a/src/Modal.vue
+++ b/src/Modal.vue
@@ -33,6 +33,23 @@
import { inRange } from './util'
import parseNumber from './parser'
+ /**
+ * MutationObserver feature detection:
+ * Detects if MutationObserver is available, return false if not.
+ * No polyfill is provided here, so height 'auto' recalculation will simply stay at its initial height (won't crash).
+ * (Provide polyfill to support IE < 11)
+ */
+ const MutationObserver = (function () {
+ const prefixes = ['', 'WebKit', 'Moz', 'O', 'Ms']
+ for (let i = 0; i < prefixes.length; i++) {
+ if (prefixes[i] + 'MutationObserver' in window) {
+ console.log('got MutationObserver:', prefixes[i] + 'MutationObserver')
+ return window[prefixes[i] + 'MutationObserver']
+ }
+ }
+ return false
+ }())
+
export default {
name: 'VueJsModal',
props: {
@@ -155,16 +172,25 @@
width: 0,
widthType: 'px',
height: 0,
- heightType: 'px'
+ heightType: 'px',
+ renderedHeight: 0
},
window: {
width: 0,
height: 0
- }
+ },
+
+ mutationObserver: null
}
},
watch: {
+ /**
+ * Sets the visibility of overlay and modal.
+ * Events 'opened' and 'closed' is called here
+ * inside `setTimeout` and `$nextTick`, after the DOM changes.
+ * This fixes `$refs.modal` `undefined` bug (fixes #15)
+ */
visible (value) {
if (value) {
this.visibility.overlay = true
@@ -173,6 +199,7 @@
this.visibility.modal = true
this.$nextTick(() => {
this.addDraggableListeners()
+ this.callAfterEvent(true)
})
}, this.delay)
} else {
@@ -182,6 +209,7 @@
this.visibility.overlay = false
this.$nextTick(() => {
this.removeDraggableListeners()
+ this.callAfterEvent(false)
})
}, this.delay)
}
@@ -211,6 +239,16 @@
console.warn(`Modal "${this.name}" has scrollable flag set to true ` +
`but height is not "auto" (${this.height})`)
}
+
+ // init MutationObserver
+ // Only observe when using height: 'auto'
+ // The callback will be called when modal DOM changes,
+ // this is for updating the `top` attribute for height 'auto' modals.
+ if (this.isAutoHeight && MutationObserver) {
+ this.mutationObserver = new MutationObserver(mutations => {
+ this.updateRenderedHeight()
+ })
+ }
},
/**
* Removes "resize" window listener
@@ -236,16 +274,12 @@
const maxLeft = window.width - trueModalWidth
const maxTop = window.height - trueModalHeight
- const minTop = this.scrollable
- ? Number.NEGATIVE_INFINITY
- : 0
-
const left = shift.left + pivotX * maxLeft
const top = shift.top + pivotY * maxTop
return {
left: inRange(0, maxLeft, left),
- top: inRange(minTop, maxTop, top)
+ top: inRange(0, maxTop, top)
}
},
/**
@@ -267,7 +301,7 @@
* Returns pixel height (if set with %) and makes sure that modal size
* fits the window.
*
- * Returns 0 if height set as "auto"
+ * Returns modal.renderedHeight if height set as "auto"
*/
trueModalHeight () {
const { window, modal, isAutoHeight, adaptive } = this
@@ -277,7 +311,8 @@
: modal.height
if (isAutoHeight) {
- return 0
+ // use renderedHeight when height 'auto'
+ return this.modal.renderedHeight
}
return adaptive
@@ -366,6 +401,8 @@
},
/**
* Event handler which is triggered on $modal.show and $modal.hight
+ * BeforeEvents: ('before-close' and 'before-open') are `$emit`ed here,
+ * but AfterEvents ('opened' and 'closed') are moved to `watch.visible`.
*/
toggle (state, params) {
const { reset, scrollable, visible } = this
@@ -374,10 +411,6 @@
? 'before-close'
: 'before-open'
- const afterEventName = visible
- ? 'closed'
- : 'opened'
-
if (beforeEventName === 'before-open') {
if (reset) {
this.setInitialSize()
@@ -388,9 +421,7 @@
if (scrollable) {
document.body.classList.add('v--modal-block-scroll')
}
- }
-
- if (beforeEventName === 'before-close') {
+ } else {
if (scrollable) {
document.body.classList.remove('v--modal-block-scroll')
}
@@ -406,10 +437,8 @@
this.$emit(beforeEventName, beforeEvent)
if (!stopEventExecution) {
- const afterEvent = this.genEventObject({ state, params })
-
this.visible = state
- this.$emit(afterEventName, afterEvent)
+ // after events are called in `watch.visible`
}
},
@@ -501,6 +530,61 @@
removeDraggableListeners () {
// console.log('removing draggable handlers')
+ },
+
+ /**
+ * 'opened' and 'closed' events are `$emit`ed here.
+ * This is called in watch.visible.
+ * Because modal DOM updates are async,
+ * wrapping afterEvents in `$nextTick` fixes `$refs.modal` undefined bug.
+ * (fixes #15)
+ */
+ callAfterEvent (state) {
+ if (state) {
+ this.observe()
+ } else {
+ this.disconnectObserver()
+ }
+ const afterEventName = state
+ ? 'opened'
+ : 'closed'
+ const afterEvent = this.genEventObject({ state })
+
+ this.$emit(afterEventName, afterEvent)
+
+ // recalculate the true modal height
+ if (state && this.isAutoHeight) {
+ this.updateRenderedHeight()
+ }
+ },
+
+ /**
+ * Update $data.modal.renderedHeight using getBoundingClientRect.
+ * This method is called when:
+ * 1. modal opened
+ * 2. MutationObserver's observe callback
+ */
+ updateRenderedHeight () {
+ this.modal.renderedHeight = this.$refs.modal.getBoundingClientRect().height
+ },
+
+ /**
+ * Start observing modal's DOM, if childList or subtree changes,
+ * the callback (registered in created) will be called.
+ */
+ observe () {
+ if (this.mutationObserver) {
+ this.mutationObserver.observe(this.$refs.modal, { childList: true, subtree: true })
+ }
+ },
+
+ /**
+ * Disconnects MutationObserver
+ */
+ disconnectObserver () {
+ if (this.mutationObserver) {
+ this.mutationObserver.disconnect()
+ }
}
}
}
@@ -537,6 +621,7 @@
.v--modal-overlay.scrollable .v--modal-box {
margin-bottom: 10px;
+ transition: top 0.2s ease;
}
.v--modal {