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 createdtoasts
Correct positioning
As shown, each one is createdtoasts
It’s going to line up to the last onetoasts
The gap here is16px
To do this we need to knowexisting çš„toasts
The 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: in
toast
Create with an automatic shutdown time and close automatically when the timer runs out. - Active close: Click the close button to close
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 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
- Will this
toast
Delete from the survivable array, and the traversal list will start from this onetoast
The position is shifted upwards. - from
<body>
Delete the Dom element from. - call
unmount()
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.