Dynamic effect preview

Recently in the graduation design, I want to add a copy of apple system side message prompt box to the graduation system, let’s have a look at the effect.

Other UI library

Those of you familiar with front-end development may have noticed that in Element UI this component is called a Notification Notification; In Bootstrap this component is called the Toasts.

start

When I first saw this component, I thought it was cool. Today, I will take you to see how I realized it step by step. If there is any wrong or can be optimized, please comment on it. 🥳 (This component is based on Vue3)

Component directory structure

Toasts | | - index. Js / / registered components, Define a global variable to invoke | | - the instance. The js / / manual before and after the instance creation logic | | -- toasts. Vue / / message prompt HTMl part | | -- toastsBus. Js / / solve vue3 removal $ON and $EMIT solutionsCopy the code

toasts.vue

Rough DOM structure

<! - the popup window - >
<div class="toast-container">
    <! -- icon -->
    <template>.</template>
    <! -- Main content
    <div class="toast-content">
        <! -- Title and countdown -->
        <div class="toast-head">.</div>
        <! -- body -->
        <div class="toast-body">.</div>
        <! -- Operation button -->
        <div class="toast-operate">.</div>
    </div>
    <! - closed - >
    <div class="toast-close">
        <i class="fi fi-rr-cross-small"></i>
    </div>
</div>
Copy the code

index.js

Register components & define global variables

Here we register the component and define global variables for invocation

import toast from './instance'
import Toast from './toasts.vue'

export default (app) => {
    // Register the component
    app.component(Toast.name, Toast);
    // Register global variables, then simply call $Toast({})
    app.config.globalProperties.$Toast = toast;
}
Copy the code

instance.js

Manually mount the instance

🌟🌟🌟 🌟🌟

First let’s learn how to manually mount components to the page

import { createApp } from 'vue';
import Toasts from './toasts'

const toasts = (options) = > {
    // Create parent container
    let root = document.createElement('div');
    document.body.appendChild(root)
    // Create the Toasts instance
    let ToastsConstructor = createApp(Toasts, options)
    // Mount the parent element
    let instance = ToastsConstructor.mount(root)
    // Throw the instance itself to vue
    return instance
}
export default toasts;
Copy the code

For each createdtoastsCorrect positioning

As shown, each one is createdtoastsIt’s going to line up to the last onetoastsThe gap here is16pxTo do this we need to knowexisting çš„toastsThe height of the.

// instance.js

// Here we need to define an array to hold the current surviving toasts
let instances = []

const toasts = (options) = >{...// Add the instance to the array
    instances.push(instance)
    
    // Rework the height
    let verticalOffset = 0
    // Iterate to obtain the height of the existing toasts and the sum of their gaps
    instances.forEach(item= > {
        verticalOffset += item.$el.offsetHeight + 16
    })
    // Add the gap required by itself
    verticalOffset += 16
    // Assign the length of the current instance to the y axis
    instance.toastPosition.y = verticalOffset
    ...
}
export default toasts;
Copy the code

Add active & timed shutdown

Let’s break down the business here first:

  • Timed shutdown: intoastCreate with an automatic shutdown time and close automatically when the timer runs out.
  • Active close: Click the close button to closetoast.

On this basis, we can add some humanized operations, such as stopping the automatic closing of a toast when the mouse moves into it (other toasts are not affected), and restarting its automatic closing when the mouse moves away.

<! -- toasts.vue -->
<template>
    <transition name="toast" @after-leave="afterLeave" @after-enter="afterEnter">
        <div ref="container" class="toast-container" :style="toastStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">.<! - closed - >
            <div class="toast-close"  @click="destruction">
                <i class="fi fi-rr-cross-small"></i>
            </div>
        </div>
    </transition>
</template>

<script>
import Bus from './toastsBus'
import {ref, computed, onMounted, onBeforeUnmount} from 'vue'
export default {
    props: {
        // Automatic shutdown time (in milliseconds)
        autoClose: {
            type: Number.default: 4500}},setup(props){
        // Whether to display
        const visible = ref(false);  
        
        // Toast container instance
        const container = ref(null);
        // Toast itself height
        const height = ref(0);
        
        / / toast position
        const toastPosition = ref({
            x: 16.y: 16
        })
        const toastStyle = computed(() = >{
            return {
                top: `${toastPosition.value.y}px`.right: `${toastPosition.value.x}px`,}})/ / id of toast
        const id = ref(' ')
        
        // Toast leaves after the animation is over
        function afterLeave(){
            // tell instance.js to close ()
            Bus.$emit('closed',id.value);
        }
        // Toast enters after the animation ends
        function afterEnter(){
            height.value = container.value.offsetHeight
        }

        / / timer
        const timer = ref(null);

        // Mouse into toast
        function clearTimer(){
             if(timer.value)
                clearTimeout(timer.value)
        }
        // Mouse over toast
        function createTimer(){
           if(props.autoClose){
                timer.value = setTimeout(() = > {
                    visible.value = false
                }, props.autoClose)
            }
        }

        / / destroy
        function destruction(){
            visible.value = false
        }
        
        onMounted(() = >{
            createTimer();
        })

        onBeforeUnmount(() = >{
            if(timer.value)
                clearTimeout(timer.value)
        })
        
        
        return {
            visible,
            container,
            height,
            toastPosition,
            toastStyle,
            id,
            afterLeave,
            afterEnter,
            timer,
            clearTimer,
            createTimer,
            destruction
        }
    }
}
</script>
Copy the code

Let’s examine the logic in instance.js when toast is closed

  1. Will thistoastDelete from the survivable array, and the traversal list will start from this onetoastThe position is shifted upwards.
  2. from<body>Delete the Dom element from.
  3. callunmount()Destroy the instance.

// instance.js
import { createApp } from 'vue';
import Toasts from './toasts'
import Bus from './toastsBus'

let instances = []
let seed = 1

const toasts = (options) = > {
    // Manually mount the instance
    let ToastsConstructor = createApp(Toasts, options)
    let instance = ToastsConstructor.mount(root)
    // Add a unique identifier to the instance
    instance.id = id
    // Display the instance
    instance.visible = true.// Listen for shutdown events from toasts. Vue
    Bus.$on('closed'.(id) = > {
        // Because all 'closed' events are listened for here, match the id ensure
        if (instance.id == id) {
            // Invoke delete logic
            removeInstance(instance)
            // Delete the DOM element on 
            document.body.removeChild(root)
            // Destroy the instance
            ToastsConstructor.unmount();
        }
    })
    
    instances.push(instance)
    return instance
}

export default toasts;

// Delete logic
const removeInstance = (instance) = > {
    if(! instance)return
    let len = instances.length
    // Find the current subscript that needs to be destroyed
    const index = instances.findIndex(item= > {
        return item.id === instance.id
    })
    // Delete from array
    instances.splice(index, 1)
    // If there are viable Toasts in the current array, move the following Toasts up to recalculate the shift
    if (len <= 1) return
    // Get the height of the deleted instance
    const h = instance.height
    // Iterate over the Toasts indexed after the deleted instance
    for (let i = index; i < len - 1; i++) {
        // Formula: the surviving instance subtracts its Y-axis offset from the deleted height and its clearance height
        instances[i].toastPosition.y = parseInt(instances[i].toastPosition.y - h - 16)}}Copy the code

The complete code

index.js

import toast from './instance'
import Toast from './toasts.vue'

export default (app) => {
    app.component(Toast.name, Toast);
    app.config.globalProperties.$Toast = toast;
}
Copy the code

toastsBus.js

import emitter from 'tiny-emitter/instance'

export default {
    $on: (. args) = >emitter.on(... args),$once: (. args) = >emitter.once(... args),$off: (. args) = >emitter.off(... args),$emit: (. args) = >emitter.emit(... args) }Copy the code

instance.js

import { createApp } from 'vue';
import Toasts from './toasts'
import Bus from './toastsBus'

let instances = []
let seed = 1

const toasts = (options) = > {
    // Create parent container
    const id = `toasts_${seed++}`
    let root = document.createElement('div');
    root.setAttribute('data-id', id)
    document.body.appendChild(root)
    let ToastsConstructor = createApp(Toasts, options)
    let instance = ToastsConstructor.mount(root)
    instance.id = id
    instance.visible = true
        // Rework the height
    let verticalOffset = 0
    instances.forEach(item= > {
        verticalOffset += item.$el.offsetHeight + 16
    })
    verticalOffset += 16

    instance.toastPosition.y = verticalOffset

    Bus.$on('closed'.(id) = > {
        if (instance.id == id) {
            removeInstance(instance)
            document.body.removeChild(root)
            ToastsConstructor.unmount();
        }
    })
    instances.push(instance)
    return instance
}

export default toasts;

const removeInstance = (instance) = > {
    if(! instance)return
    let len = instances.length
    const index = instances.findIndex(item= > {
        return item.id === instance.id
    })
    instances.splice(index, 1)
    if (len <= 1) return
    const h = instance.height
    for (let i = index; i < len - 1; i++) {
        instances[i].toastPosition.y = parseInt(instances[i].toastPosition.y - h - 16)}}Copy the code

toast.vue

Add hundreds of millions of details, such as icon can be customized or pictures, can cancel the close button, set the automatic close duration, or disable the automatic close function.

<template>
<transition name="toast" @after-leave="afterLeave" @after-enter="afterEnter">
  <! - the popup window - >
  <div ref="container" class="toast-container" :style="toastStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
    <! -- icon -->
    <template v-if="type || type ! = 'custom' || type ! = 'img'">
        <div class="toast-icon success" v-if="type==='success'">
            <i class="fi fi-br-check"></i>
        </div>
        <div class="toast-icon warning" v-if="type==='warning'">
            ?
        </div>
        <div class="toast-icon info" v-if="type==='info'">
            <i class="fi fi-sr-bell-ring"></i>
        </div>
        <div class="toast-icon error" v-if="type==='error'">
            <i class="fi fi-br-cross-small"></i>
        </div>
    </template>
    <div :style="{'backgroundColor': customIconBackground}" class="toast-icon" v-if="type==='custom'" v-html="customIcon"></div>
    <img class="toast-custom-img" :src="customImg" v-if="type==='img'"/>
    <! -- content -->
    <div class="toast-content">
        <! -- head -->
        <div class="toast-head" v-if="title">
            <! -- title -->
            <span class="toast-title">{{title}}</span>
            <! -- time -->
            <span class="toast-countdown">{{countDown}}</span>
        </div>
        <! -- body -->
        <div class="toast-body" v-if="message" v-html="message"></div>
        <! -- operate -->
        <div class="toast-operate">
            <a class="toast-button-confirm" 
               :class="[{'success':type==='success'}, {'warning':type==='warning'}, {'info':type==='info'}, {'error':type==='error'}]">{{confirmText}}</a>
        </div>
    </div>
    <! - closed - >
    <div v-if="closeIcon" class="toast-close"  @click="destruction">
        <i class="fi fi-rr-cross-small"></i>
    </div>
  </div>
  </transition>
</template>

<script>
import Bus from './toastsBus'
import {ref, computed, onMounted, onBeforeUnmount} from 'vue'
export default {
    props: {
        title: String.closeIcon: {
            type: Boolean.default: true
        },
        message: String.type: {
            type: String.validator: function(val) {
                return ['success'.'warning'.'info'.'error'.'custom'.'img'].includes(val); }},confirmText: String.customIcon: String.customIconBackground: String.customImg: String.autoClose: {
            type: Number.default: 4500}},setup(props){
        / / show
        const visible = ref(false);

        // Container instance
        const container = ref(null);

        / / height
        const height = ref(0);

        / / position
        const toastPosition = ref({
            x: 16.y: 16
        })
        const toastStyle = computed(() = >{
            return {
                top: `${toastPosition.value.y}px`.right: `${toastPosition.value.x}px`,}})/ / the countdown
        const countDown = computed(() = >{
            return '2 seconds ago'
        })

        const id = ref(' ')

        // After leaving
        function afterLeave(){
            Bus.$emit('closed',id.value);
        }
        // After entering
        function afterEnter(){
            height.value = container.value.offsetHeight
        }

        / / timer
        const timer = ref(null);

        // Mouse entry
        function clearTimer(){
             if(timer.value)
                clearTimeout(timer.value)
        }
        // Mouse over
        function createTimer(){
           if(props.autoClose){
                timer.value = setTimeout(() = > {
                    visible.value = false
                }, props.autoClose)
            }
        }

        / / destroy
        function destruction(){
            visible.value = false
        }

        onMounted(() = >{
            createTimer();
        })

        onBeforeUnmount(() = >{
            if(timer.value)
                clearTimeout(timer.value)
        })

        return {
            visible,
            toastPosition,
            toastStyle,
            countDown,
            afterLeave,
            afterEnter,
            clearTimer,
            createTimer,
            timer,
            destruction,
            container,
            height,
            id
        }
    }
}
</script>

<style lang="scss" scoped>// External container.toast-container{
    width: 330px;
    box-shadow: rgba(0.0.0.0.1) 0px 2px 12px 0px;
    background-color: rgba(#F7F7F7.6);
    border: 1px solid #E5E5E5;
    padding: 14px 13px;
    z-index: 1001;
    position: fixed;
    top: 0;
    right: 0;
    border-radius: 10px;
    backdrop-filter: blur(15px);
    display: flex;
    align-items: stretch;
    transition: all .3sease; will-change: top,left; } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -icon --------------
.toast-icon..toast-close{
    flex-shrink: 0;
}
.toast-icon{
    width: 30px;
    height: 30px;
    border-radius: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center; } / / correct.toast-icon.success{
    background-color: rgba(#2BB44A.15);
    color: #2BB44A; } / / exception.toast-icon.warning{
    background-color: rgba(#ffcc00.15);
    color: #F89E23;
    font-weight: 600;
    font-size: 18px; } / / errors.toast-icon.error{
    font-size: 18px;
    background-color: rgba(#EB2833.1);
    color: #EB2833; } / / information.toast-icon.info{
    background-color: rgba(#3E71F3.1);
    color: #3E71F3; } // Customize the image.toast-custom-img{
    width: 40px;
    height: 40px;
    border-radius: 10px;
    overflow: hidden;
    flex-shrink: 0; } / / -- -- -- -- -- -- -- -- -- -- -- -- --content -----------
.toast-content{
    padding: 0 8px 0 13px;
    flex: 1;
}
// -------------- head --------------
.toast-head{
    display: flex;
    align-items: center;
    justify-content: space-between;
}
// title
.toast-title{
    font-size: 16px;
    line-height: 24px;
    color: # 191919;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap; } / /time
.toast-countdown{
    font-size: 12px;
    color: # 929292;
    line-height: 18.375 px.; } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- --body -----------
.toast-body{
    color: # 191919;
    line-height: 21px;
    padding-top: 5px;
}
// ---------- close -------
.toast-close{
    padding: 3px;
    cursor: pointer;
    font-size: 18px;
    width: 24px;
    height: 24px;
    border-radius: 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.toast-close:hover{
    background-color: rgba(#E4E4E4.5);
}
// --------- operate ----------
.toast-button-confirm{
    font-weight: 600;
    color: #3E71F3;
}
.toast-button-confirm:hover{
    color: #345ec9; } / / success.toast-button-confirm.success{
    color: #2BB44A;
}
.toast-button-confirm.success:hover{
    color: #218a3a; } / / exception.toast-button-confirm.warning{
    color: #F89E23;
}
.toast-button-confirm.warning:hover{
    color: #df8f1f; } / / information.toast-button-confirm.info{
    color: #3E71F3;
}
.toast-button-confirm.info:hover{
    color: #345ec9; } / / errors.toast-button-confirm.error{
    color: #EB2833;
}
.toast-button-confirm.error:hover{
    color: #c9101a;
}


/ * * / animation
.toast-enter-from..toast-leave-to{
  transform: translateX(120%);
}
.v-leave-from..toast-enter-to{
  transform: translateX(00%);
}
</style>
Copy the code

main.js

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

import '@/assets/font/UIcons/font.css'

/ / install toasts
import toasts from './components/toasts'

app.use(toasts).mount('#app')
Copy the code

use

<template>
    <button @click="clickHandle">send</button>
</template>

<script>
import { getCurrentInstance } from 'vue'
export default {
  setup(){
    const instance = getCurrentInstance()
    function clickHandle(){
      // call vue3 global variable
      instance.appContext.config.globalProperties.$Toast({
        type: 'info'.title: 'Here's a headline.'.message: 'This article is the main logic of the mount function to clarify the basic processing flow (Vue 3.1.1). '})}return {
      clickHandle
    }
  }
}
</script>
Copy the code

Icon Icon font

www.flaticon.com/

Your praise is my biggest motivation, thank you for watching. Comments will be private GitHub address.