通用弹窗

说明

移动端 h5 中,弹窗会处于全屏固定定位的蒙层之上。

但是在默认情况下,以下情况,会导致蒙层下面的内容也会随之滑动(参见示例一):

  • 弹窗内容元素外:在蒙层元素上滚动
  • 弹窗内容元素内:
    • 在不可滚动的元素上滚动
    • 在可滚动的内容滚动到顶部/底部后,继续滚动

通常情况下,蒙层之下的内容随之滚动不是我们所需要的效果。

因此,通用弹窗组件就是解决这个问题的,该组件支持:

  • 可选择是否禁止蒙层之下的内容滚动
  • 可允许弹窗内的内容滚动

示例

PS:请在手机端打开该页面进行体验

示例一:允许蒙层之下的内容滚动

示例二:禁止蒙层之下的内容滚动

调用

<common-popup
    v-model="true"
    :forbid-bg-scroll="true"
    scroll-area-selector=".scroll-area"
    :show-close="true"
    @close="hide">
    <div class="common-popup-slot">
        用户自定义的 slot 内容,需要自己写样式
    </div>
</common-popup>
1
2
3
4
5
6
7
8
9
10

参数说明

特性 attribute

  • v-modelBoolean类型,控制弹窗是否显示,默认为false
  • forbid-bg-scrollBoolean类型,控制是否禁止蒙层之下的内容滚动,默认为true
  • scroll-area-selectorString类型,弹窗内可滚动的元素选择器,比如.scroll-area,仅在forbid-bg-scrolltrue时有效
  • show-closeBoolean类型,是否显示关闭按钮

事件

  • close事件:点击“关闭 ❎”按钮时触发

代码

调用示例

<template>
    <div class="common-popup-example">
        <button @click="show">
            打开弹窗
        </button>
        <CommonPopup
            v-model="isShow"
            :forbid-bg-scroll="forbidBgScroll"
            scroll-area-selector=".scroll-area"
            :show-close="true"
            @close="hide"
        >
            <div class="common-popup-slot">
                <h1 class="title">
                    弹窗标题
                </h1>
                <ul class="scroll-area">
                    <li
                        v-for="i in 30"
                        :key="i"
                    >
                        {{ i }}、这里是列表项
                    </li>
                </ul>
            </div>
        </CommonPopup>
    </div>
</template>

<script>
import CommonPopup from './index.vue';
export default {
    name: 'CommonPopupExample',
    components: {
        CommonPopup
    },
    props: {
        forbidBgScroll: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            isShow: false
        };
    },
    methods: {
        show() {
            this.isShow = true;
        },
        hide() {
            this.isShow = false;
        }
    }
};
</script>

<style lang="less">
.common-popup-slot {
    h1,
    ul,
    li {
        margin: 0;
        padding: 0;
    }
}
</style>
<style lang="less" scoped>
.common-popup-slot {
    width: 300px;
    background-color: #ffe5ae;
    > .title {
        font-weight: bold;
        font-size: 16px;
        line-height: 40px;
        text-align: center;
    }
    > .scroll-area {
        height: 280px;
        overflow: scroll;
        -webkit-overflow-scrolling: touch;
    }
}
</style>
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

组件源码

<template>
    <div
        v-show="modelValue"
        ref="mask"
        class="common-popup-mask"
    >
        <div class="common-popup-content">
            <a
                v-if="showClose"
                class="close-btn"
                @click="close"
            />
            <slot />
        </div>
    </div>
</template>

<script>
import { contains } from '../../common/js/util';

const CLOSE = 'update:modelValue';
export default {
    name: 'CommonPopup',
    props: {
        modelValue: {
            type: Boolean,
            required: false,
            default: false
        },
        showClose: {
            type: Boolean,
            required: false,
            default: true
        },
        // 禁止弹窗任何区域滚动
        forbidBgScroll: {
            type: Boolean,
            required: false,
            default: true
        },
        // 滚动区域 DOM 元素的类名,仅在 forbidBgScroll 为真值时有效
        scrollAreaSelector: {
            type: String,
            required: false,
            default: ''
        }
    },
    data() {
        return {
            // 滚动区域 DOM 元素
            scrollArea: '',
            // 滚动开始时的 Y 坐标
            touchStartY: 0,
            // 滚动区域 offsetHeight、scrollHeight
            scrollAreaOffsetHeight: 0,
            scrollAreaScrollHeight: 0
        };
    },
    mounted() {
        if (this.forbidBgScroll) {
            this.scrollArea =
                this.scrollAreaSelector &&
                this.$refs.mask.querySelector(this.scrollAreaSelector);
            this.bindForbidBgScrollEvent();
        }
    },
    destroyed() {
        if (this.forbidBgScroll) {
            this.removeForbidBgScrollEvent();
        }
    },
    methods: {
        close() {
            this.$emit(CLOSE, false);
        },
        bindForbidBgScrollEvent() {
            this.$refs.mask &&
                this.$refs.mask.addEventListener(
                    'touchmove',
                    this.maskTouchMove
                );
            if (this.scrollArea) {
                this.scrollArea.addEventListener(
                    'touchstart',
                    this.scrollAreaTouchStart
                );
                this.scrollArea.addEventListener(
                    'touchmove',
                    this.scrollAreaTouchMove
                );
            }
        },
        removeForbidBgScrollEvent() {
            this.$refs.mask &&
                this.$refs.mask.removeEventListener(
                    'touchmove',
                    this.maskTouchMove
                );
            if (this.scrollArea) {
                this.scrollArea.removeEventListener(
                    'touchstart',
                    this.scrollAreaTouchStart
                );
                this.scrollArea.removeEventListener(
                    'touchmove',
                    this.scrollAreaTouchMove
                );
            }
        },
        maskTouchMove(evt) {
            const target = evt.target;
            if (!this.scrollArea || !contains(this.scrollArea, target)) {
                evt.preventDefault();
            }
        },
        scrollAreaTouchStart(evt) {
            const targetTouches = evt.targetTouches || [];
            if (targetTouches.length > 0) {
                const touch = targetTouches[0] || {};
                this.touchStartY = touch.clientY;
                this.scrollAreaOffsetHeight = this.scrollArea.offsetHeight;
                this.scrollAreaScrollHeight = this.scrollArea.scrollHeight;
            }
        },
        scrollAreaTouchMove(evt) {
            const changedTouches = evt.changedTouches;
            let canMove = false;
            const scrollTop = this.scrollArea.scrollTop;
            if (changedTouches.length > 0) {
                const touch = changedTouches[0] || {};
                const moveY = touch.clientY;

                if (moveY > this.touchStartY && scrollTop <= 0) {
                    canMove = false;
                } else if (
                    moveY < this.touchStartY &&
                    scrollTop + this.scrollAreaOffsetHeight >=
                    this.scrollAreaScrollHeight
                ) {
                    canMove = false;
                } else {
                    canMove = true;
                }
                if (!canMove) {
                    evt.preventDefault();
                }
            }
        }
    }
};
</script>

<style lang="less" scoped>
.common-popup-mask {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.6);
    z-index: 1000;
    > .common-popup-content {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        > .close-btn {
            position: absolute;
            top: -54px;
            right: -26px;
            width: 34px;
            height: 34px;
            background: url('./popup-close-btn.png') top left/100% no-repeat;
        }
    }
}
</style>
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