Using vue3 to imitate the side message prompt effect of Apple system

Using vue3 to imitate the side message prompt effect of Apple system

Animation Preview

I am working on my graduation project recently. I want to add a side message prompt box similar to the Apple system to the graduation project system. Let's take a look at the effect first.

Other UI Libraries

Students who are familiar with front-end development may have discovered that this component is called Notification in Element UI and Toasts in Bootstrap.

start

When I first saw this component, I thought it was very cool. Today, I will show you how I implemented it step by step. If there are any mistakes or areas that can be optimized, please comment. πŸ₯³ (This component is implemented based on Vue3)

Component directory structure

Toasts

|

| -- index.js // Register components and define global variables for easy calling

|

| -- instance.js // Logic before and after manual instance creation

|

| -- toasts.vue // Message prompt HTML part

|

| -- toastsBus.js // Solution to remove $on and $emit in vue3

toasts.vue

Approximate DOM structure

<!-- Pop-up window -->
<div class="toast-container">
    <!-- Icon icon -->
    <template>
        ...
    </template>
    <!-- Main content -->
    <div class="toast-content">
        <!-- Title and countdown -->
        <div class="toast-head">
            ...
        </div>
        <!-- body -->
        <div class="toast-body">...</div>
        <!-- Action Button -->
        <div class="toast-operate">
            ...
        </div>
    </div>
    <!-- Close -->
    <div class="toast-close">
        <i class="fi fi-rr-cross-small"></i>
    </div>
</div>

index.js

Register components & define global variables

Here we register the component and define global variables for calling

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

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

instance.js

Manually mount an instance

🌟🌟🌟 Here is the key point of the full article🌟🌟🌟

First, let's learn how to manually mount components to the page.

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

const toasts = (options) => {
    // Create a parent container let root = document.createElement('div');
    document.body.appendChild(root)
    // Create a 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;

Correct positioning for each toast created

As shown in the figure, each toast created will be arranged below the previous toast (the gap here is 16px). To achieve this effect we need to know the height of the existing toasts.

// instance.js

// Here we need to define an array to store the currently alive toasts
let instances = []

const toasts = (options) => {
    ...
    // After creation, add the instance to the array instances.push(instance)
    
    // Reset height let verticalOffset = 0
    // Traverse and get the height of the currently existing toasts and their gap accumulation instances.forEach(item => {
        verticalOffset += item.$el.offsetHeight + 16
    })
    // Accumulate the gap required verticalOffset += 16
    // Assign the current instance's y-axis length instance.toastPosition.y = verticalOffset
    ...
}
export default toasts;

Added active & timed shutdown function

Let's first analyze the business here:

  • Timed closing: Give an automatic closing time when toast is created, and it will automatically close when the timer ends.
  • Active close: Click the close button to close the toast.

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 re-enabling its automatic closing when the mouse moves out.

<!-- 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">
            ...
            <!-- Close -->
            <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);
        // The height of the toast itself 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`,
            }
        })
        
        //toast id
        const id = ref('')
        
        //After the toast leaves the animation, function afterLeave(){
            // Tell instance.js that it needs to be closed()
            Bus.$emit('closed',id.value);
        }
        //After the toast enters the animation, function afterEnter(){
            height.value = container.value.offsetHeight
        }

        // Timer const timer = ref(null);

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

        //Destruction 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>

Let's analyze the logic of toast closing in instance.js

  1. Delete this toast from the survival array, and traverse the array to shift the toast position upward starting from this one.
  2. Remove the DOM element from the <body>.
  3. Call unmount() to 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 an 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 the closing event from toasts.vue Bus.$on('closed', (id) => {
        // Because all 'closed' events will be monitored here, the id must be matched to ensure if (instance.id == id) {
            //Call the deletion logic removeInstance(instance)
            // Delete the DOM element on <body> document.body.removeChild(root)
            //Destroy the instance ToastsConstructor.unmount();
        }
    })
    
    instances.push(instance)
    return instance
}

export default toasts;

// Deletion logic const removeInstance = (instance) => {
    if (!instance) return
    let len ​​= instances.length
    // Find the index that needs to be destroyed const index = instances.findIndex(item => {
        return item.id === instance.id
    })
    // Remove instances from the array.splice(index, 1)
    // If there are still surviving toasts in the current array, you need to traverse and move the following toasts up, and recalculate the displacement if (len <= 1) return
    // Get the height of the deleted instance const h = instance.height
    // Traverse the Toasts subscripted after the deleted instance
    for (let i = index; i < len - 1; i++) {
        // Formula: The surviving instance subtracts the deleted height and its gap height from its y-axis offset instances[i].toastPosition.y = parseInt(instances[i].toastPosition.y - h - 16)
    }
}

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;
}

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)
}

instance.js

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

let instances = []
let seed = 1

const toasts = (options) => {
    // Create a 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
        // Reset 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)
    }
}

toast.vue

Add a little bit of details, such as customizable icon or image, cancel close button, set auto close time, or disable auto close function.

<template>
<transition name="toast" @after-leave="afterLeave" @after-enter="afterEnter">
  <!-- Pop-up 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>
    <!-- Close -->
    <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){
        // Display 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`,
            }
        })

        // 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 enters function clearTimer(){
             if(timer.value)
                clearTimeout(timer.value)
        }
        //Mouse out function createTimer(){
           if(props.autoClose){
                timer.value = setTimeout(() => {
                    visible.value = false
                }, props.autoClose)
            }
        }

        //Destruction 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 .3s ease;
    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;
}
// Error.toast-icon.error{
    font-size: 18px;
    background-color: rgba(#EB2833, .1);
    color: #EB2833;
}
// information.toast-icon.info{
    background-color: rgba(#3E71F3, .1);
    color: #3E71F3;
}
// Custom 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.375px;
}
//-------------- 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;
}
// Error.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>

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')

use

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

<script>
import { getCurrentInstance } from 'vue'
export default {
  setup(){
    const instance = getCurrentInstance()
    function clickHandle(){
      // It's a shame to call vue3's global variables here. I don't know if you guys have any other good ideas.instance.appContext.config.globalProperties.$Toast({
        type: 'info',
        title: 'This is a title',
        message: 'This article is to sort out the main logic of the mount function, aiming to clarify the basic processing flow (Vue 3.1.1 version). '
      })
    }
    return {
      clickHandle
    }
  }
}
</script>

Get icon font

www.flaticon.com/

Summarize

This is the end of this article about using vue3 to imitate the side message prompt effect of the Apple system. For more relevant vue3 imitating Apple's side message prompt content, please search 123WORDPRESS.COM's previous articles or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • How to use Vue3 management system to implement dynamic routing and dynamic side menu bar
  • Vue3.0 responsive system source code line by line analysis
  • Vue3.x Minimum Prototype System Explanation

<<:  Use PS to create an xhtml+css website homepage in two minutes

>>:  Customize the style of the <input type="file"> element used when uploading files in HTML

Recommend

Example code for implementing the wavy water ball effect using CSS

Today I learned a new CSS special effect, the wav...

Vue.js implements simple timer function

This article example shares the specific code of ...

Example of utf8mb4 collation in MySQL

Common utf8mb4 sorting rules in MySQL are: utf8mb...

Summary of some HTML code writing style suggestions

Omit the protocol of the resource file It is reco...

Linux lossless expansion method

Overview The cloud platform customer's server...

jQuery to achieve the barrage effect case

This article shares the specific code of jQuery t...

CSS to achieve the effect of rotating flip card animation

The css animation of the rotating flip effect, th...

Solution to the problem that Java cannot connect to MySQL 8.0

This article shares a collection of Java problems...

Detailed explanation of non-parent-child component value transfer in Vue3

Table of contents App.vue sub1.vue sub2.vue Summa...

MySql sets the specified user database view query permissions

1. Create a new user: 1. Execute SQL statement to...

vsftpd virtual user based on MySql authentication

Table of contents 1. MySQL installation 1.2 Creat...

Detailed explanation of custom instructions for Vue.js source code analysis

Preface In addition to the default built-in direc...

10 skills that make front-end developers worth millions

The skills that front-end developers need to mast...

Complete steps to install FFmpeg in CentOS server

Preface The server system environment is: CentOS ...

MySQL scheduled database backup operation example

This article describes the example of MySQL sched...