vue3-modal-manager
Version:
A flexible and powerful modal manager for Vue 3
616 lines (615 loc) • 22.9 kB
JavaScript
var b = Object.defineProperty;
var v = (g, t, e) => t in g ? b(g, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : g[t] = e;
var l = (g, t, e) => v(g, typeof t != "symbol" ? t + "" : t, e);
import { createVNode as S, render as f } from "vue";
class L {
/**
* 初始化函數
* @param currentModal modal
*/
constructor(t) {
l(this, "currentModal");
l(this, "resizeHandlers", /* @__PURE__ */ new Map());
l(this, "resizing", !1);
l(this, "resizeDirection", "");
l(this, "initialSize", {
width: 0,
height: 0,
left: 0,
top: 0,
right: 0,
bottom: 0,
mouseLimits: {
limitX: { min: 0, max: 0 },
limitY: { min: 0, max: 0 },
limitXY: { x: { min: 0, max: 0 }, y: { min: 0, max: 0 } }
}
});
l(this, "initialMouse", { x: 0, y: 0 });
/**
* 處理調整大小
*/
l(this, "handleResize", (t) => {
this.resizing && (t.preventDefault(), requestAnimationFrame(() => {
this.currentModal.container.style.transform !== "" && (this.currentModal.container.style.transform = "", this.currentModal.container.style.inset = `${this.initialSize.top}px ${this.initialSize.right}px ${this.initialSize.bottom}px ${this.initialSize.left}px`);
const e = t.clientX, i = t.clientY;
let n = e - this.initialMouse.x, o = i - this.initialMouse.y;
switch (this.resizeDirection) {
// 單向水平調整 (e, w)
case "e":
case "w": {
const r = this.calcMouseMoveRange();
n = Math.max(r.min, Math.min(n, r.max));
break;
}
// 單向垂直調整 (n, s)
case "n":
case "s": {
const r = this.calcMouseMoveRange();
o = Math.max(r.min, Math.min(o, r.max));
break;
}
// 雙向調整 (se, sw, ne, nw)
case "se":
case "sw":
case "ne":
case "nw": {
const r = this.calcMouseMoveRange();
n = Math.max(r.x.min, Math.min(n, r.x.max)), o = Math.max(r.y.min, Math.min(o, r.y.max));
break;
}
}
let s = this.initialSize.width, a = this.initialSize.height, m = this.initialSize.left, c = this.initialSize.top, d = this.initialSize.right, h = this.initialSize.bottom;
switch (this.resizeDirection) {
// 單向水平調整 (e, w)
case "e":
s = Math.max(100, this.initialSize.width + n), d = this.initialSize.right - (s - this.initialSize.width);
break;
case "w":
s = Math.max(100, this.initialSize.width - n), m = this.initialSize.left + (this.initialSize.width - s);
break;
// 單向垂直調整 (n, s)
case "s":
a = Math.max(100, this.initialSize.height + o), h = this.initialSize.bottom - (a - this.initialSize.height);
break;
case "n":
a = Math.max(100, this.initialSize.height - o), c = this.initialSize.top + (this.initialSize.height - a);
break;
// 雙向調整 (se, sw, ne, nw)
case "se":
s = Math.max(100, this.initialSize.width + n), a = Math.max(100, this.initialSize.height + o), d = this.initialSize.right - (s - this.initialSize.width), h = this.initialSize.bottom - (a - this.initialSize.height);
break;
case "sw":
s = Math.max(100, this.initialSize.width - n), a = Math.max(100, this.initialSize.height + o), m = this.initialSize.left + (this.initialSize.width - s), h = this.initialSize.bottom - (a - this.initialSize.height);
break;
case "ne":
s = Math.max(100, this.initialSize.width + n), a = Math.max(100, this.initialSize.height - o), d = this.initialSize.right - (s - this.initialSize.width), c = this.initialSize.top + (this.initialSize.height - a);
break;
case "nw":
s = Math.max(100, this.initialSize.width - n), a = Math.max(100, this.initialSize.height - o), m = this.initialSize.left + (this.initialSize.width - s), c = this.initialSize.top + (this.initialSize.height - a);
break;
}
const u = this.currentModal.container.querySelector(
`#${this.currentModal.id}`
);
u.style.width = `${s}px`, u.style.height = `${a}px`, this.currentModal.container.style.inset = `${c}px ${d}px ${h}px ${m}px`, this.emitResizeEvent(this.currentModal, s, a);
}));
});
/**
* 停止調整大小
*/
l(this, "stopResize", () => {
if (!this.resizing) return;
this.currentModal.container.classList.remove("modal-resizing");
const t = parseFloat(this.currentModal.container.style.width), e = parseFloat(this.currentModal.container.style.height);
this.emitResizeEndEvent(this.currentModal, t, e), this.resizing = !1, this.resizeDirection = "", document.removeEventListener("mousemove", this.handleResize), document.removeEventListener("mouseup", this.stopResize);
});
this.currentModal = t, this.createResizeHandles(t.id);
}
/**
* 設置當前modal
* @param currentModal modal
*/
setCurrentModal(t) {
this.currentModal = t;
}
/**
* 建立resize handle
* @param modalId modalId
*/
createResizeHandles(t) {
const e = ["e", "s", "se", "w", "n", "ne", "sw", "nw"], i = this.currentModal.container;
if (!i) {
console.warn(`Modal element with id ${t} not found`);
return;
}
i.querySelectorAll(".resize-handle").length > 0 || e.forEach((o) => {
const s = document.createElement("div");
switch (s.setAttribute("modalResizeArea", t), s.setAttribute("data-direction", o), s.className = `resize-handle resize-${o}`, Object.assign(s.style, {
position: "absolute",
background: "transparent",
pointerEvents: "auto",
zIndex: "1000"
}), o) {
case "e":
Object.assign(s.style, {
top: "0",
right: "0",
width: "5px",
height: "100%",
cursor: "e-resize"
});
break;
case "s":
Object.assign(s.style, {
bottom: "0",
left: "0",
width: "100%",
height: "5px",
cursor: "s-resize"
});
break;
case "se":
Object.assign(s.style, {
bottom: "0",
right: "0",
width: "10px",
height: "10px",
cursor: "se-resize"
});
break;
case "w":
Object.assign(s.style, {
top: "0",
left: "0",
width: "5px",
height: "100%",
cursor: "w-resize"
});
break;
case "n":
Object.assign(s.style, {
top: "0",
left: "0",
width: "100%",
height: "5px",
cursor: "n-resize"
});
break;
case "ne":
Object.assign(s.style, {
top: "0",
right: "0",
width: "10px",
height: "10px",
cursor: "ne-resize"
});
break;
case "sw":
Object.assign(s.style, {
bottom: "0",
left: "0",
width: "10px",
height: "10px",
cursor: "sw-resize"
});
break;
case "nw":
Object.assign(s.style, {
top: "0",
left: "0",
width: "10px",
height: "10px",
cursor: "nw-resize"
});
break;
}
i.appendChild(s);
});
}
/**
* 綁定調整大小功能
* @param modalId 模態框ID
*/
bindResize(t) {
const e = this.currentModal.container.querySelectorAll(
`[modalResizeArea="${t}"]`
);
if (e.length === 0) {
console.warn(`No resize handles found for modal ${t}`);
return;
}
e.forEach((i) => {
const n = i.dataset.direction || "", o = (a) => this.startResize(a, n), s = `${t}-${n}`;
this.resizeHandlers.set(s, o), i.addEventListener("mousedown", o);
});
}
/**
* 開始調整大小
* @param e 滑鼠事件
* @param direction 方向
*/
startResize(t, e) {
t.preventDefault(), t.stopPropagation(), this.initialMouse = {
x: t.clientX,
y: t.clientY
};
const i = this.currentModal.container.getBoundingClientRect(), n = this.currentModal.boxElement.getBoundingClientRect(), s = window.getComputedStyle(this.currentModal.container).transform, a = s !== "none" && s !== "";
let m, c, d, h;
if (a) {
const r = n.left + n.width / 2, x = n.top + n.height / 2;
m = r - i.width / 2 - n.left, c = x - i.height / 2 - n.top, d = n.width - (m + i.width), h = n.height - (c + i.height);
} else
m = i.left - n.left, c = i.top - n.top, d = n.width - (m + i.width), h = n.height - (c + i.height);
const u = {
limitX: {
min: n.left + 5,
max: n.width + n.left - 5
},
limitY: {
min: n.top + 5,
max: n.height + n.top - 5
},
limitXY: {
x: { min: n.left + 9, max: n.width + n.left - 9 },
y: { min: n.top + 9, max: n.height + n.top - 9 }
}
};
this.initialSize = {
width: i.width,
height: i.height,
left: m,
top: c,
right: d,
bottom: h,
mouseLimits: u
}, this.resizing = !0, this.resizeDirection = e, document.addEventListener("mousemove", this.handleResize), document.addEventListener("mouseup", this.stopResize), this.currentModal.container.classList.add("modal-resizing");
}
/**
* 計算滑鼠移動範圍
* @returns movementRange | movementRangeXY
*/
calcMouseMoveRange() {
switch (this.resizeDirection) {
// 單向水平調整 (e, w)
case "e":
return {
min: 100 - this.initialSize.width,
max: this.initialSize.mouseLimits.limitX.max - this.initialMouse.x
};
case "w":
return {
min: this.initialSize.mouseLimits.limitX.min - this.initialMouse.x,
max: -(100 - this.initialSize.width)
};
// 單向垂直調整 (n, s)
case "s":
return {
min: 100 - this.initialSize.height,
max: this.initialSize.mouseLimits.limitY.max - this.initialMouse.y
};
case "n":
return {
min: this.initialSize.mouseLimits.limitY.min - this.initialMouse.y,
max: -(100 - this.initialSize.height)
};
// 雙向調整 (se, sw, ne, nw)
case "se":
// 東南
case "sw":
// 西南
case "ne":
// 東北
case "nw":
const t = {
x: {
min: 0,
max: 0
},
y: {
min: 0,
max: 0
}
};
return this.resizeDirection.includes("e") ? (t.x.min = 100 - this.initialSize.width, t.x.max = this.initialSize.mouseLimits.limitXY.x.max - this.initialMouse.x) : (t.x.min = this.initialSize.mouseLimits.limitXY.x.min - this.initialMouse.x, t.x.max = -(100 - this.initialSize.width)), this.resizeDirection.includes("s") ? (t.y.min = 100 - this.initialSize.height, t.y.max = this.initialSize.mouseLimits.limitXY.y.max - this.initialMouse.y) : (t.y.min = this.initialSize.mouseLimits.limitXY.y.min - this.initialMouse.y, t.y.max = -(100 - this.initialSize.height)), t;
default:
return { min: 0, max: 0 };
}
}
/**
* 發出尺寸變更事件
* @param modal modal
* @param width modal寬度
* @param height modal高度
*/
emitResizeEvent(t, e, i) {
const n = new CustomEvent("modal-resize", {
detail: {
modalId: t.id,
width: e,
height: i
}
});
t.container.dispatchEvent(n);
}
/**
* 發出調整結束事件
* @param modal modal
* @param width modal寬度
* @param height modal高度
*/
emitResizeEndEvent(t, e, i) {
const n = new CustomEvent("modal-resize-end", {
detail: {
modalId: t.id,
width: e,
height: i
}
});
t.container.dispatchEvent(n);
}
/**
* 解除調整大小綁定
* @param modalId modalId
*/
unbindResize(t) {
this.currentModal.container.querySelectorAll(
`[modalResizeArea="${t}"]`
).forEach((n) => {
const o = n.dataset.direction || "", s = `${t}-${o}`, a = this.resizeHandlers.get(s);
a && (n.removeEventListener("mousedown", a), this.resizeHandlers.delete(s));
});
const i = this.currentModal.container.querySelector(
`#${t} > div[style*="pointer-events: none"]`
);
i && i.remove();
}
}
class z {
constructor(t) {
l(this, "appContext");
l(this, "modalRoutes");
l(this, "ModalList");
l(this, "baseZIndex", 1e3);
l(this, "movingModalId");
l(this, "initPosition");
/**
* 開始拖曳
* @param e 滑鼠事件
* @param modalId modal Id
* @returns
*/
l(this, "startDrag", (t, e) => {
this.movingModalId = e;
const i = this.ModalList.get(this.movingModalId);
if (!i) return;
this.bringToFront(e);
const n = i.container.getBoundingClientRect();
this.initPosition = {
initialMouseX: t.clientX,
initialMouseY: t.clientY,
initialModalX: n.left,
initialModalY: n.top
}, document.addEventListener("mousemove", this.calcMouseDisplacement), document.addEventListener("mouseup", this.stopDrag), t.preventDefault();
});
/**
* 計算拖曳modal元件的位移
* @param e 滑鼠事件
* @returns
*/
l(this, "calcMouseDisplacement", (t) => {
const e = this.ModalList.get(this.movingModalId), { initialMouseX: i, initialMouseY: n, initialModalX: o, initialModalY: s } = this.initPosition;
if (!e) return;
const a = t.clientX - i, m = t.clientY - n;
let c = o + a, d = s + m;
const h = e.boxElement.getBoundingClientRect(), u = e.container.getBoundingClientRect(), r = h.left, x = h.right - u.width, M = h.top, p = h.bottom - u.height;
c = Math.max(r, Math.min(c, x)), d = Math.max(M, Math.min(d, p));
const y = c - h.left, w = d - h.top;
e.container.style.left = `${y}px`, e.container.style.top = `${w}px`, e.container.style.transform = "none";
});
/**
* 停止拖曳
*/
l(this, "stopDrag", () => {
document.removeEventListener("mousemove", this.calcMouseDisplacement), document.removeEventListener("mouseup", this.stopDrag);
});
this.modalRoutes = t, this.ModalList = /* @__PURE__ */ new Map(), this.appContext = null, this.movingModalId = "", this.initPosition = {};
}
install(t) {
t.config.globalProperties.$modalManager = this, t.provide("modalManager", this);
const e = t.mount;
t.mount = (...i) => {
const n = e.call(t, ...i);
return setTimeout(() => {
t._container && t._container._vnode ? this.appContext = t._container._vnode.appContext : console.warn("Modal Manager: Unable to get appContext");
}, 0), n;
};
}
//==========================================創建modal外層元素===================================================
/**
* 建立modal ID
* @param name modal元件名稱
* @returns modal ID
*/
generateId(t) {
const e = Date.now(), i = Math.floor(Math.random() * 1e3);
return `${t}-${e}-${i}`;
}
/**
* 建立modal div(固定)元素
* @param modalId modal ID
* @returns HTMLDivElement
*/
createDivByFixed(t) {
const e = document.createElement("div");
return e.setAttribute("data-modalId", t), e.style.position = "fixed", e.style.zIndex = "1000", e.style.top = "50%", e.style.left = "50%", e.style.transform = "translate(-50%, -50%)", e.style.width = "100%", e.style.height = "100%", e.style.display = "flex", e.style.alignItems = "center", e.style.justifyContent = "center", e.style.backgroundColor = "rgba(0, 0, 0, 0.6)", e;
}
/**
* 建立modal div(懸浮)元素
* @param modalId modal ID
* @returns HTMLDivElement
*/
createDraggableDiv(t) {
const e = document.createElement("div"), i = window.scrollX + window.innerWidth / 2, n = window.scrollY + window.innerHeight / 2;
return e.setAttribute("data-modalId", t), e.style.position = "absolute", e.style.zIndex = `${this.baseZIndex + this.ModalList.size}`, e.style.left = `${i}px`, e.style.top = `${n}px`, e.style.transform = "translate(-50%, -50%)", e.style.width = "max-content", e.style.height = "max-content", e.style.borderRadius = "10px", e.style.boxShadow = "rgba(0, 0, 0, 0.3) 0px 19px 38px, rgba(0, 0, 0, 0.22) 0px 15px 12px", e;
}
//==========================================開啟 modal===================================================
/**
* 打開固定位置的視窗
* @param name modal 名稱
* @param params modal 參數
* @returns 回傳一個包含結果資訊的 Promise:
* - `success`:是否成功開啟
* - `msg`:訊息
* - `modalId`:此 modal 的唯一 ID
*/
async openModal(t, e) {
return this.createModal(t, 0, e);
}
/**
* 打開可拖動的視窗
* @param name modal 名稱
* @param params modal 參數
* @returns 回傳一個包含結果資訊的 Promise:
* - `success`:是否成功開啟
* - `msg`:訊息
* - `modalId`:此 modal 的唯一 ID
*/
async openPopover(t, e) {
return [...this.ModalList.values()].find((n) => n.name === t) ? (console.warn(`Modal ${t} is existing`), { success: !1, msg: `Modal ${t} is existing`, modalId: "" }) : this.createModal(t, 1, e);
}
/**
* 打開可拖動的視窗(多)
* @param name modal 名稱
* @param params modal 參數
* @returns 回傳一個包含結果資訊的 Promise:
* - `success`:是否成功開啟
* - `msg`:訊息
* - `modalId`:此 modal 的唯一 ID
*/
async openPopovers(t, e) {
return this.createModal(t, 1, e);
}
async createModal(t, e, i) {
const n = this.modalRoutes.find((o) => o.name === t);
if (!n)
return console.error(`Modal ${t} not found`), { success: !1, msg: "not found Modal", modalId: "" };
try {
const o = this.generateId(t), s = e === 0 ? this.createDivByFixed(o) : this.createDraggableDiv(o), a = i != null && i.id ? document.getElementById(i.id) : null;
a ? (a.style.position = "relative", a.appendChild(s)) : document.body.appendChild(s), this.ModalList.set(o, {
id: o,
container: s,
name: t,
boxElement: a || document.body
});
const c = (await n.component()).default, d = {
...i == null ? void 0 : i.props,
modalId: o,
onClosed: () => this.closeModal(o)
};
i != null && i.events && Object.entries(i.events).forEach(([u, r]) => {
const x = `on${u.charAt(0).toUpperCase() + u.slice(1)}`;
d[x] = r;
});
const h = S(c, d);
if (this.appContext && (h.appContext = this.appContext), f(h, s), e === 1) {
((i == null ? void 0 : i.drag) ?? !1) && this.bindDrag(o);
const r = (i == null ? void 0 : i.position) ?? null;
if (r && this.setPosition(o, r), !!(i != null && i.resize)) {
const M = this.ModalList.get(o);
M && (M.resize = new L(M), M.resize.bindResize(o));
}
}
return { success: !0, msg: "success", modalId: o };
} catch (o) {
return console.error(`Failed to load modal ${t}:`, o), { success: !1, msg: "Failed to load modal", modalId: "" };
}
}
//==========================================關閉modal===================================================
/**
* 關閉modal
* @param modalId modal Id
* @returns
*/
closeModal(t) {
const e = this.ModalList.get(t);
if (!e) return;
f(null, e.container), e.container.parentNode && e.container.parentNode.removeChild(e.container), e.boxElement.querySelectorAll("div[data-modalId]").length === 0 && (e.boxElement.style.position = ""), this.ModalList.delete(t);
}
/**
* 關閉所有modal
*/
removeAllModal() {
[...this.ModalList.values()].forEach((e) => {
this.closeModal(e.id);
}), console.log("%c 移除所有modal", "color: green; font-size: 14px");
}
//==========================================modal拖曳功能===================================================
/**
* 綁定拖曳功能
* modal元件需在拖曳處加上modalDraggableArea='modalId'
* @param modalId modal Id
* @returns
*/
bindDrag(t) {
const e = this.ModalList.get(t);
e && (e.header = e.container.querySelector(
`[modalDraggableArea = ${t}]`
), e.header.style.cursor = "move", e.handleDrag || (e.handleDrag = (i) => this.startDrag(i, t)), e.header.addEventListener("mousedown", e.handleDrag));
}
/**
* 解除拖曳功能
* @param modalId modal Id
* @returns
*/
unbindDrag(t) {
const e = this.ModalList.get(t);
!e || !e.header || (e.header.style.cursor = "", e.handleDrag && e.header.removeEventListener("mousedown", e.handleDrag));
}
/**
* 計算modal元件階層
* @param modalId modal Id
* @returns
*/
bringToFront(t) {
const e = this.ModalList.get(t);
if (!e) return;
const i = parseInt(e.container.style.zIndex), n = this.baseZIndex + this.ModalList.size;
i !== n && this.ModalList.forEach((o) => {
const s = parseInt(o.container.style.zIndex);
o.id === t ? (o.container.style.zIndex = `${n}`, o.container.style.boxShadow = "rgba(0, 0, 0, 0.3) 0px 19px 38px, rgba(0, 0, 0, 0.22) 0px 15px 12px") : s > i && (o.container.style.zIndex = `${s - 1}`, o.container.style.boxShadow = "");
});
}
//==========================================modal位置===================================================
/**
* 指定視窗生成位置
* @param modalId modal Id
* @param position 位置座標
* @returns
*/
setPosition(t, e) {
const i = this.ModalList.get(t);
i && (i.container.style.top = e.top ?? "auto", i.container.style.bottom = e.bottom ?? "auto", i.container.style.left = e.left ?? "auto", i.container.style.right = e.right ?? "auto", i.container.style.transform = "");
}
/**
* 移動視窗至指定容器
* @param modalId
* @param target
* @returns
*/
moveToLayers(t, e) {
const i = this.ModalList.get(t);
if (!i) return;
const n = document.getElementById(e);
n && (i.boxElement.style.position = "", n.style.position = "relative", n.appendChild(i.container), i.container.style.top = "50%", i.container.style.left = "50%", i.container.style.transform = "translate(-50%, -50%)", i.boxElement = n, i.resize && i.resize.setCurrentModal(i));
}
}
l(z, "modalManager");
const $ = (g) => {
const t = new z(g);
return z.modalManager = t, t;
}, D = () => z.modalManager;
export {
$ as createModalManager,
D as useModalManager
};