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

Detailed explanation of Javascript closures and applications

Table of contents Preface 1. What is a closure? 1...

Nest.js authorization verification method example

Table of contents 0x0 Introduction 0x1 RBAC Imple...

Three common uses of openlayers6 map overlay (popup window marker text)

Table of contents 1. Write in front 2. Overlay to...

Four ways to compare JavaScript objects

Table of contents Preface Reference Comparison Ma...

JavaScript to achieve drop-down menu effect

Use Javascript to implement a drop-down menu for ...

Use js to write a simple snake game

This article shares the specific code of a simple...

Detailed explanation of Vue save automatic formatting line break

I searched for many ways to change it online but ...

Use of filter() array filter in JS

Table of contents 1. Introduction 2. Introduction...

Solution to Chinese garbled characters when operating MySQL database in CMD

I searched on Baidu. . Some people say to use the...

How to load the camera in HTML

Effect diagram: Overall effect: Video loading: Ph...

Vue/react single page application back without refresh solution

Table of contents introduction Why bother? Commun...

In-depth understanding of MySQL slow query log

Table of contents What is the slow query log? How...

The final solution to Chrome's minimum font size limit of 12px

I believe that many users who make websites will ...

Solve the problem of inconsistent MySQL storage time

After obtaining the system time using Java and st...