UNPKG

vue3-modal-manager

Version:

A flexible and powerful modal manager for Vue 3

616 lines (615 loc) 22.9 kB
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 };