Let’s take a look at the final result

Code address: vue 2 x | vue 3. X

Preview address: example

Inspired by Ant-Design-UI, I think this kind of operation is more suitable for interactive use, so I encapsulate a popbox component by following this interactive mode.

I’m going to use it firstvue 2.xWriting to explain, involving andvue 3.xThe different code implementations are explained.

Basic popbox function

Let’s start with the layout of the next basic Dialog function

<template>
    <div class="dialog" title="Whole frame. - Also a mask layer.">
        <div class="dialog-content" title="White frame box">
            <header title="Content header">
                <h2 title="Dynamic incoming headers"></h2>
                <button title="Close button"></button>
            </header>
            <div class="dialog-body" title="Slot occupied Area - Main area of popbox contents">
                <slot></slot>
            </div>
            <footer title="Bottom area of the frame">
                <slot name="footer"></slot>
            </footer>
        <div>
    </div>
</template>
<style>
.dialog {
    display: flex;
    align-items: center; // Center the middle content verticallyjustify-content: center; // Center the middle content section horizontallywidth: 100%;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    background-color: rgba(0.0.0.0.5);
}
.dialog-content {
    border-radius: 2px; 
    box-shadow: 0px 1px 5px 0px rgba(0.0.0.0.2), 0px 2px 2px 0px rgba(0.0.0.0.14), 0px 3px 1px -2px rgba(0.0.0.0.12); 
    background-color: #fff;
    overflow: hidden;
    display: flex;
    flex-direction: column; // Vertical layoutmax-height: 90vh; // Set the maximum height}.dialog-body {
    flex: 1; // Automatic elastic heightoverflow: auto; // When the height exceeds the limit, the scroll appears}</style>
Copy the code

Now that you have the basic layout ready, you can do some dynamic things: for example, if the whole thing fades out, you can wrap a
tag around the whole thing, so that you can switch between show and hide and fade out. Then add a click close event action like this:

<template>
    <transition name="fade">
        <div class="dialog" v-show="value" title="Whole frame. - Also a mask layer." @click="onClose">
            <div class="dialog-content" title="White frame box">
                <header title="Content header">
                    <h2 title="Dynamic incoming headers"></h2>
                    <button @click="onClose" ref="close-btn" title="Close button"></button>
                </header>. omit<div>
        </div>
    </transition>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

/** Basic popup component */
@Component({
    name: "base-dialog"
})
export default class BaseDialog extends Vue {
    /** Two-way binding shows hidden values */
    @Prop({
        type: Boolean.default: false}) value! : boolean;/** Can the Dialog be closed by clicking on the mask layer
    @Prop({
        type: Boolean.default: true}) closeByMask! : boolean/** whether the Dialog itself is inserted on the body element. Nested 'Dialog' must specify this property and assign it to 'true' */
    @Prop({
        type: Boolean.default: false}) appendToBody! : boolean; $refs! : {"close-btn": HTMLElement
    }
    
    onClose(e: MouseEvent) {
        // The reason @click.stop is not used is because, in the case of nesting, events prevent bubbling,
        // Will cause upward-passing events to stop capturing, so check whether it is a mask layer or a close button
        if ((e && e.target === this.$el && this.closeByMask) || (e && e.target === this.$refs["close-btn"]) {this.$emit("input".false);
            this.$emit("close"); }}mounted() {
        if (this.appendToBody) {
            // Move the node to  after initialization
            this.$el.remove();
            document.body.appendChild(this.$el); }}beforeDestroy() {
        this.timer && clearTimeout(this.timer);
        this.appendToBody && this.$el.remove(); // The node inserted into the body should be removed separately}}</script>
<style>. Omit duplicate code.fade-enter-active..fade-leave-active {
    transition: all .3s;
}
.fade-enter..fade-leave-active {
    opacity: 0;
}
</style>
Copy the code

In this way, the component is used to implement the fade effect. The basic configuration of some props, such as width and title, is not expanded. Then look at the current effects:

Address positioning hierarchy issues

It is well known that there may be multiple pop-up layers, regardless of whether they are in the same node level, which are displayed in the order that the code is written. So you have to deal with this hierarchy of positioning. This hierarchy can be set to a common unique variable that is incremented by each component invocation or instantiation, so that the next component to be called will be in the same position as the previous one:

<template>
    <transition name="fade">
        <div class="dialog" v-show="value" @click="onClose" :style="{ 'z-index': zIndex }">. omit</div>
    </transition>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

/** Global location hierarchy, accumulated for each component used */
let zIndex = 1000;

/** Basic popup component */
@Component({
    name: "base-dialog"
})
export default class BaseDialog extends Vue {
    zIndex = zIndex;
    
    created(){ zIndex++; }}Copy the code

Interactive animation implementation

  1. The above steps have implemented a basic popbox function operation, the next step is to deal with the middlecontentPart of the animation; Here is the animation process:

Because the trajectory of the animation is from Scale (0) translate3D (dynamic X, dynamic Y, 0) to Scale (1) translate3D (0, 0, 0) So we can define a transition end csS-class first:

.opened {
    transform: translate3d(0.0.0) scale(1) ! important;
}
Copy the code

Then dynamically set the left and right translation position through JS, and finally add.Opened to Content.

  1. jsTo process the logic of click coordinates, the first step is to get the position of the click, which can be passeddocument.addEventListener("click", fn)To deal with,fn(ev: MouseEvent)In theev.pageYandev.pageXIs the current mouse click relative to the screen position. After obtaining the mouse position, convert the center point (i.e. the pan position of the content) to the following code:
<template>
    <transition name="fade">
        <div class="dialog" v-show="value" @click="onClose" :style="{ 'z-index': zIndex }">
            <div
                :class="['dialog-content', { 'moving': contentMove }, { 'opened': contentShow }]"
                :style="{ 'transform': `translate3d(${contentX}, ${contentY}, 0) scale(0)` }"
            >. omit<div>
        </div>
    </transition>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";

const isFirefox = navigator.userAgent.toLocaleLowerCase().indexOf("firefox") > 0;

@Component({
    name: "base-dialog"
})
export default class BaseDialog extends Vue {... Omit duplicate code/** Content box 'x' axis offset */
    private contentX = "0";

    /** Content box 'y' axis offset */
    private contentY = "0";
    
    /** The offset position needs to be set dynamically, so after setting the position, control the node to switch animation */
    private contentShow = false;

    /** Content box transition animation */
    private contentMove = false;
    
    @Watch("value")
    onValue(val: boolean) {
        this.timer && clearTimeout(this.timer);
        if(! val) {this.contentShow = false; } } private timer! : NodeJS.Timeout;/** * Sets the location of the content area *@param E Mouse events */
    private setContentPosition(e: MouseEvent) {
        // Coordinates will only be recorded if the external click is closed
        if (!this.value || this.contentShow || this.$el.contains(e.target as HTMLElement)) return;
        this.contentMove = false;
        const { clientWidth, clientHeight } = this.$el;
        const centerX = clientWidth / 2;
        const centerY = clientHeight / 2;
        const pageY = e.pageY - centerY;
        const pageX = e.pageX - centerX;
        this.contentX = `${pageX / clientWidth * 100}vw`;
        this.contentY = `${pageY / clientHeight * 100}vh`;
        // Set transition animation after cSS3 animation life cycle ends
        this.timer = setTimeout(() = > {
            this.contentMove = true;
            this.contentShow = true;
        }, isFirefox ? 100 : 0); // Firefox has a bug that requires a delay of 100 ms
    }
    
    mounted(){... Omit duplicate codedocument.addEventListener("click".this.setContentPosition);
    }

    beforeDestroy(){... Omit duplicate codedocument.removeEventListener("click".this.setContentPosition); }}</script>
<style>. Omit duplicate code.dialog-content.opened {
    transform: translate3d(0.0.0) scale(1) ! important;
}
.dialog-content.moving {
    transition: 0.3 s all;
}
</style>
Copy the code

Take a look at the setContentPosition method logic

Calculation of offset coordinates: Since the coordinates of the click event are calculated from the upper left corner of the screen, and our layout is always centered screen, the base coordinate of the center of the screen is the current mask spread across the screen, that is, the width and height of this.$el divided by half; The last click of the coordinate minus the center point is the offset position, and finally calculate the 100 percent ratio. The reason I use VW/Vh as the final unit here is that it can be scaled up as the screen dynamically shrinks without affecting the animation offset.

Document monitor event propagation order: setContentPosition if (! this.value… The reason for the inversion is that the click triggers the inside of the page and then bubbles up to the document, so it should be inverted

Delay setting transition animation styles: CSS also has a life cycle, so no transition animation can be created before setting the coordinates, otherwise it will move from the middle to the click position, so set the coordinates and set the transition animation after the life cycle is over.

At this point the whole function is done.

Different from Vue 3.x

In Vue3, it is not possible to move the current component node directly to a location like this:

el.remove();
document.body.appendChild(el);
Copy the code

Direct operations will make the node of the slot unresponsive and error, so you need to wrap a

tag around the component, specifying insertion to a certain location, but this is redundant. The same code is written twice, like this:

    <teleport to="body" v-if="appendToBody">
        <transition name="fade">
            <div class="dialog" v-show="value" @click="onClose" :style="{ 'z-index': zIndex }">. omit<div>
        </transition>
    </teleport>
    <transition name="fade" v-else>
        <div class="dialog" v-show="value" @click="onClose" :style="{ 'z-index': zIndex }">. omit<div>
    </transition>
Copy the code

Vue2 did not come simply, directly behind the way I try to use JSX to bypass the template method and found that “the transition > in the JSX performance behavior and the behavior of the template tags, specific see code comments dialog. The TSX. So vuE3 is currently written as a single file template (but not as elegant as VUE2).