Appearance
一个模态框的完整封装
通过函数调用,即可渲染一个模态框组件。模态框里面渲染的内容由传入的参数决定的,传入什么组件就渲染什么内容。
传入的实参组件内部想要关闭模态框只需要使用emit('close')
分发自定义事件即可。
简单版本
ts
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";
/**
* 渲染模态框
* @param content 模态框内容组件
* @param props 模态框内容组件的props
*/
export function renderModal(content: Component, props: Record<any, any>) {
// 容器
const mask = document.createElement("div");
mask.classList.add("modal-mask-container");
// 模态框的class
const classList = ref(["modal-container", "modal-enter"]);
const className = computed(() => classList.value.join(" "));
// 模态框虚拟DOM
const container = h(
// 定义组件
{
name: "ModalContainer",
setup(_, context) {
return () =>
h(
"div",
{
onClick(e: Event) {
// 停止事件发生
e.stopPropagation();
},
class: className.value,
},
[renderSlot(context.slots, "default")]
);
},
},
null,
// 使用组件
{
default: () =>
h(content, {
...props,
onClose: handleUnload,
}),
}
);
// 卸载容器
const handleUnload = () => {
classList.value.push("modal-leave");
setTimeout(() => {
// 移除整个模态框
mask.remove();
}, 300);
};
// 点击遮罩层关闭容器
mask.onclick = handleUnload;
// 渲染
render(container, mask);
// 去除入场动画
setTimeout(() => {
classList.value.pop();
}, 300);
// 挂载
document.body.appendChild(mask);
}
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";
/**
* 渲染模态框
* @param content 模态框内容组件
* @param props 模态框内容组件的props
*/
export function renderModal(content: Component, props: Record<any, any>) {
// 容器
const mask = document.createElement("div");
mask.classList.add("modal-mask-container");
// 模态框的class
const classList = ref(["modal-container", "modal-enter"]);
const className = computed(() => classList.value.join(" "));
// 模态框虚拟DOM
const container = h(
// 定义组件
{
name: "ModalContainer",
setup(_, context) {
return () =>
h(
"div",
{
onClick(e: Event) {
// 停止事件发生
e.stopPropagation();
},
class: className.value,
},
[renderSlot(context.slots, "default")]
);
},
},
null,
// 使用组件
{
default: () =>
h(content, {
...props,
onClose: handleUnload,
}),
}
);
// 卸载容器
const handleUnload = () => {
classList.value.push("modal-leave");
setTimeout(() => {
// 移除整个模态框
mask.remove();
}, 300);
};
// 点击遮罩层关闭容器
mask.onclick = handleUnload;
// 渲染
render(container, mask);
// 去除入场动画
setTimeout(() => {
classList.value.pop();
}, 300);
// 挂载
document.body.appendChild(mask);
}
复杂版本
ts
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";
/**
* 渲染模态框
* @param content 模态框内容组件
* @param props 模态框内容组件的props
* @param offset 动画开始时模态框的偏移量
*/
export function renderModal(
content: Component,
props: Record<any, any>,
offset?: {
x: number;
y: number;
}
) {
// 容器
let mask = document.createElement("div");
mask.classList.add("modal-mask-container");
// 模态框的class
const classList = ref(["modal-container"]);
const className = computed(() => classList.value.join(" "));
// 设置动画偏移量
let ob = new ResizeObserver(([entry]) => {
if (offset !== undefined) {
// 若传入了模态框起始偏移量
mask.style.setProperty("--top", `${offset.y}px`);
mask.style.setProperty("--left", `${offset.x}px`);
mask.style.setProperty("--width", `${entry.contentRect.width}px`);
mask.style.setProperty("--height", `${entry.contentRect.height}px`);
classList.value.push("modal-enter-offset");
} else {
// 无偏移量,居中进场、离场
classList.value.push("modal-enter");
}
// 断开监听
ob.disconnect();
});
// 模态框虚拟DOM
let container = h(
// 定义组件
{
name: "ModalContainer",
setup(_, context) {
return () =>
h(
"div",
{
onClick(e: Event) {
// 停止事件发生
e.stopPropagation();
},
class: className.value,
},
[renderSlot(context.slots, "default")]
);
},
},
null,
// 使用组件
{
default: () =>
h(content, {
...props,
onClose: handleUnload,
}),
}
);
// 卸载容器
const handleUnload = () => {
offset
? classList.value.push("modal-leave-offset")
: classList.value.push("modal-leave");
setTimeout(() => {
// 移除整个模态框
mask && mask.remove();
// 释放内存
// @ts-ignore
mask = null;
// @ts-ignore
container = null;
// @ts-ignore
ob = null;
}, 280);
};
// 点击遮罩层关闭容器
mask.onclick = handleUnload;
// 渲染
render(container, mask);
// 渲染后才能监听
ob.observe(container.el as HTMLElement);
// 去除入场动画
setTimeout(() => {
if (
mask &&
(classList.value.includes("modal-enter") ||
classList.value.includes("modal-enter-offset"))
)
classList.value.pop();
}, 300);
// 挂载
document.body.appendChild(mask);
}
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";
/**
* 渲染模态框
* @param content 模态框内容组件
* @param props 模态框内容组件的props
* @param offset 动画开始时模态框的偏移量
*/
export function renderModal(
content: Component,
props: Record<any, any>,
offset?: {
x: number;
y: number;
}
) {
// 容器
let mask = document.createElement("div");
mask.classList.add("modal-mask-container");
// 模态框的class
const classList = ref(["modal-container"]);
const className = computed(() => classList.value.join(" "));
// 设置动画偏移量
let ob = new ResizeObserver(([entry]) => {
if (offset !== undefined) {
// 若传入了模态框起始偏移量
mask.style.setProperty("--top", `${offset.y}px`);
mask.style.setProperty("--left", `${offset.x}px`);
mask.style.setProperty("--width", `${entry.contentRect.width}px`);
mask.style.setProperty("--height", `${entry.contentRect.height}px`);
classList.value.push("modal-enter-offset");
} else {
// 无偏移量,居中进场、离场
classList.value.push("modal-enter");
}
// 断开监听
ob.disconnect();
});
// 模态框虚拟DOM
let container = h(
// 定义组件
{
name: "ModalContainer",
setup(_, context) {
return () =>
h(
"div",
{
onClick(e: Event) {
// 停止事件发生
e.stopPropagation();
},
class: className.value,
},
[renderSlot(context.slots, "default")]
);
},
},
null,
// 使用组件
{
default: () =>
h(content, {
...props,
onClose: handleUnload,
}),
}
);
// 卸载容器
const handleUnload = () => {
offset
? classList.value.push("modal-leave-offset")
: classList.value.push("modal-leave");
setTimeout(() => {
// 移除整个模态框
mask && mask.remove();
// 释放内存
// @ts-ignore
mask = null;
// @ts-ignore
container = null;
// @ts-ignore
ob = null;
}, 280);
};
// 点击遮罩层关闭容器
mask.onclick = handleUnload;
// 渲染
render(container, mask);
// 渲染后才能监听
ob.observe(container.el as HTMLElement);
// 去除入场动画
setTimeout(() => {
if (
mask &&
(classList.value.includes("modal-enter") ||
classList.value.includes("modal-enter-offset"))
)
classList.value.pop();
}, 300);
// 挂载
document.body.appendChild(mask);
}
css
.modal-mask-container {
position: fixed;
inset: 0;
z-index: 2025;
background-color: var(--mask-color);
animation: fade var(--time-fast) 1 ease;
color: var(--text-color-1);
display: flex;
align-items: center;
justify-content: center;
}
.modal-leave-offset {
animation: modalOffset var(--time-normal) 1 ease reverse;
}
.modal-enter-offset {
animation: modalOffset var(--time-normal) 1 ease;
}
@keyframes modalOffset {
from {
position: absolute;
/*故意拉的偏移量,最好是获取模态框大小/2*/
top: calc(var(--top) - calc(var(--height) / 2));
left: calc(var(--left) - calc(var(--width) / 2));
transform: scale(0.3);
opacity: 0.3;
}
to {
position: absolute;
top: 50%;
left: 50%;
transform: scale(1) translate(-50%, -50%);
opacity: 1;
}
}
.modal-leave {
animation: modal var(--time-normal) 1 ease reverse;
}
.modal-enter {
animation: modal var(--time-normal) 1 ease;
}
@keyframes modal {
from {
opacity: 0.3;
transform: scale(0.3);
}
}
@keyframes fade {
from {
background-color: #00000000;
}
to {
background-color: var(--mask-color);
}
}
.modal-mask-container {
position: fixed;
inset: 0;
z-index: 2025;
background-color: var(--mask-color);
animation: fade var(--time-fast) 1 ease;
color: var(--text-color-1);
display: flex;
align-items: center;
justify-content: center;
}
.modal-leave-offset {
animation: modalOffset var(--time-normal) 1 ease reverse;
}
.modal-enter-offset {
animation: modalOffset var(--time-normal) 1 ease;
}
@keyframes modalOffset {
from {
position: absolute;
/*故意拉的偏移量,最好是获取模态框大小/2*/
top: calc(var(--top) - calc(var(--height) / 2));
left: calc(var(--left) - calc(var(--width) / 2));
transform: scale(0.3);
opacity: 0.3;
}
to {
position: absolute;
top: 50%;
left: 50%;
transform: scale(1) translate(-50%, -50%);
opacity: 1;
}
}
.modal-leave {
animation: modal var(--time-normal) 1 ease reverse;
}
.modal-enter {
animation: modal var(--time-normal) 1 ease;
}
@keyframes modal {
from {
opacity: 0.3;
transform: scale(0.3);
}
}
@keyframes fade {
from {
background-color: #00000000;
}
to {
background-color: var(--mask-color);
}
}