sodialog 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # SoDialog
2
+
3
+ 基于 HTML5 `dialog` 的可复用弹窗库,支持 **Modal** 与 **Offcanvas**,并通过 JavaScript 动态创建 HTML 元素。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install sodialog
9
+ ```
10
+
11
+ ## 使用
12
+
13
+ ```ts
14
+ import { openModal, openOffcanvas } from 'sodialog'
15
+ import 'sodialog/style.css'
16
+
17
+ openModal({
18
+ id: 'order-delete',
19
+ title: '提示',
20
+ position: 'center',
21
+ animation: 'fade',
22
+ useModal: true,
23
+ draggable: true,
24
+ dragHandle: 'header',
25
+ autoFitSize: true,
26
+ content: '<p>这是 Modal</p>',
27
+ confirmText: '确定',
28
+ cancelText: '取消',
29
+ })
30
+
31
+ openOffcanvas({
32
+ title: '侧边栏',
33
+ placement: 'end',
34
+ animation: 'slide',
35
+ content: '<p>这是 Offcanvas</p>',
36
+ })
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `openModal(options)`
42
+
43
+ - `title: string`
44
+ - `content: string | Node`
45
+ - `id?: string`(默认自动生成。传入后同 ID Modal 不重复创建,再次调用会唤起已有实例)
46
+ - `onCreated?: (handle) => void`(仅新建时触发,可读取自动生成的 `handle.id`)
47
+ - `onReused?: (handle) => void`(仅复用已有同 ID 实例时触发)
48
+ - `handle.refit(): void`(手动触发一次尺寸重算)
49
+ - `position?: 'center' | 'top' | 'bottom'` (默认 `center`)
50
+ - `animation?: 'slide' | 'fade' | 'zoom'` (默认 `fade`)
51
+ - `useModal?: boolean` (默认 `true`,`true` 使用 `showModal()`,`false` 使用 `show()`)
52
+ - `draggable?: boolean` (默认 `false`)
53
+ - `dragHandle?: 'header' | 'title' | 'body' | 'panel' | string` (默认 `header`,也可传 CSS 选择器)
54
+ - `autoFitSize?: boolean` (默认 `true`,会根据 body 内容变化自动扩/缩尺寸,例如图片加载完成后)
55
+ - `scrollMode?: 'body' | 'hybrid' | 'viewport' | 'none'` (默认 `body`)
56
+ - `hybridSwitchRatio?: number` (默认 `1.35`,仅 `scrollMode: 'hybrid'` 时生效,最小值 `1`)
57
+ - `autoFitUseScrollbar?: boolean` (兼容旧参数。`false` 等价于 `scrollMode: 'viewport'`)
58
+ - `refitOnContentChange?: boolean` (默认 `true`,内容变化/图片加载后自动触发尺寸重算)
59
+ - `autoFitMinWidth?: number` (默认 `280`)
60
+ - `autoFitMinHeight?: number` (默认 `160`)
61
+ - `confirmText?: string`
62
+ - `cancelText?: string`
63
+ - `confirmAction?: 'hide' | 'destroy'`(默认 `hide`;显式传入 `id` 时默认 `destroy`)
64
+ - `closeOnBackdrop?: boolean` (默认 `true`)
65
+ - `closeOnEsc?: boolean` (默认 `true`)
66
+ - `onConfirm?: () => void`
67
+ - `onCancel?: () => void`
68
+
69
+ 说明:
70
+
71
+ - `hybrid` 表示先使用 body 内滚动;当内容高度远超可视区阈值时自动切到外层 viewport 滚动
72
+ - `取消/关闭` 语义是 `dialog.close()`(隐藏)
73
+ - `confirmAction: 'destroy'` 语义是 `dialog.remove()`(销毁)
74
+
75
+ ### `openOffcanvas(options)`
76
+
77
+ 在 `openModal` 参数基础上新增:
78
+
79
+ - `placement?: 'start' | 'end' | 'top' | 'bottom'` (默认 `end`)
80
+ - `animation?: 'slide' | 'fade' | 'zoom'` (默认 `slide`)
81
+
82
+ 示例(类似 Bootstrap Offcanvas 多位置):
83
+
84
+ ```ts
85
+ openOffcanvas({ title: 'Left', placement: 'start', animation: 'slide', content: '<p>Left</p>' })
86
+ openOffcanvas({ title: 'Right', placement: 'end', animation: 'slide', content: '<p>Right</p>' })
87
+ openOffcanvas({ title: 'Top', placement: 'top', animation: 'fade', content: '<p>Top</p>' })
88
+ openOffcanvas({ title: 'Bottom', placement: 'bottom', animation: 'zoom', content: '<p>Bottom</p>' })
89
+ ```
90
+
91
+ ### `SoDialog.open(options)`
92
+
93
+ 通用入口,`options.kind` 可为 `modal` 或 `offcanvas`。
94
+
95
+ ## 开发
96
+
97
+ ```bash
98
+ npm run dev
99
+ npm run lint
100
+ npm run build
101
+ ```
102
+
103
+ Demo 中已包含:
104
+
105
+ - 预设默认值:`center`、`zoom`、`medium`、`showModal`、不可拖动、自动适配
106
+ - `Modal ID` 留空自动生成,输入后可复用唤醒同 ID
107
+ - Modal 内切换不同尺寸图片,验证 `autoFitSize` 自动扩缩
108
+ - 包含超大图(`xlarge`)、超长单词、超宽表格的溢出测试
109
+ - Modal 内容按钮打开子窗口(支持 ID 复用唤起)
110
+ - 子窗口包含多种表单元素(input/select/checkbox/textarea)演示
111
+ - 新增 Markdown 编辑器子窗口(工具栏插入、实时预览)
112
+ - 编辑器支持“仅编辑 / 编辑 + 预览”模式切换
113
+
114
+ ## 发布到 NPM
115
+
116
+ 1. 登录:`npm login`
117
+ 2. 检查包名可用性
118
+ 3. 发布:`npm publish --access public`
119
+
120
+ > 当前样式均使用 `sod-` 前缀命名,避免污染全局 `body/:root/*`。
@@ -0,0 +1 @@
1
+ .sod-dialog{border:0;padding:0;margin:auto;background:transparent;max-width:none;max-height:none;overflow:visible}.sod-dialog::backdrop{background:#0000007f}.sod-panel{box-sizing:border-box;background:#fff;color:#212529;border-radius:.5rem;box-shadow:0 .5rem 1rem #00000040;min-width:min(520px,calc(100vw - 2rem));max-width:min(520px,calc(100vw - 2rem))}.sod-modal .sod-panel{position:relative;margin:0}.sod-modal[open]{width:100vw;height:100vh;display:flex;justify-content:center;align-items:center}.sod-modal[open].sod-modal-viewport-scroll{align-items:flex-start;overflow:auto;padding:1rem 0}.sod-modal .sod-panel.sod-modal-pos-center{align-self:center}.sod-modal .sod-panel.sod-modal-pos-top{align-self:flex-start;margin-top:1rem}.sod-modal .sod-panel.sod-modal-pos-bottom{align-self:flex-end;margin-bottom:1rem}.sod-modal .sod-panel.sod-modal-autofit{min-width:0;width:auto;height:auto;max-width:calc(100vw - 2rem);max-height:calc(100vh - 2rem);display:flex;flex-direction:column}.sod-modal .sod-panel.sod-modal-draggable{touch-action:none}.sod-modal .sod-panel.sod-is-dragging{animation:none!important;transition:none!important}.sod-modal .sod-panel .sod-drag-handle{cursor:move;-webkit-user-select:none;user-select:none}.sod-modal .sod-panel .sod-drag-handle.is-dragging{cursor:grabbing}.sod-modal .sod-panel.sod-modal-anim-fade{animation:sod-fade-in .18s ease-out}.sod-modal .sod-panel.sod-modal-anim-fade.is-closing{animation:sod-fade-out .18s ease-in forwards}.sod-modal .sod-panel.sod-modal-anim-zoom{animation:sod-modal-zoom-in .18s ease-out}.sod-modal .sod-panel.sod-modal-anim-zoom.is-closing{animation:sod-modal-zoom-out .18s ease-in forwards}.sod-modal .sod-panel.sod-modal-anim-slide{animation:sod-modal-slide-in .18s ease-out}.sod-modal .sod-panel.sod-modal-anim-slide.is-closing{animation:sod-modal-slide-out .18s ease-in forwards}.sod-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem .75rem;border-bottom:1px solid #dee2e6}.sod-title{margin:0;font-size:1.1rem}.sod-close{border:0;background:transparent;color:#6c757d;font-size:1.5rem;line-height:1;cursor:pointer}.sod-body{box-sizing:border-box;padding:1rem;overflow-wrap:anywhere;word-break:break-word}.sod-body>*{max-width:100%}.sod-body input,.sod-body select,.sod-body textarea,.sod-body button{max-width:100%;box-sizing:border-box}.sod-body img,.sod-body video,.sod-body canvas,.sod-body iframe,.sod-body table{max-width:100%}.sod-modal .sod-panel.sod-modal-autofit .sod-body{flex:1 1 auto;min-height:0}.sod-body p{margin:0 0 .5rem}.sod-body ul{margin:.5rem 0 0;padding-left:1.2rem}.sod-footer{padding:.75rem 1rem 1rem;border-top:1px solid #dee2e6;display:flex;justify-content:flex-end;gap:.5rem}.sod-btn{display:inline-flex;align-items:center;justify-content:center;border-radius:.375rem;border:1px solid transparent;padding:.5rem 1rem;font-size:.95rem;font-weight:500;cursor:pointer;transition:all .15s ease-in-out}.sod-btn:focus-visible{outline:2px solid #86b7fe;outline-offset:1px}.sod-btn-primary{background:#0d6efd;color:#fff;border-color:#0d6efd}.sod-btn-primary:hover{background:#0b5ed7;border-color:#0a58ca}.sod-btn-outline{background:transparent;color:#6c757d;border-color:#6c757d}.sod-btn-outline:hover{color:#fff;background:#6c757d}.sod-offcanvas[open]{width:100vw;height:100vh}.sod-offcanvas .sod-panel{position:fixed;margin:0;min-width:0;max-width:none;border-radius:0;width:min(360px,90vw);height:100vh}.sod-offcanvas .sod-panel.sod-placement-start{left:0;top:0}.sod-offcanvas .sod-panel.sod-placement-end{right:0;top:0}.sod-offcanvas .sod-panel.sod-placement-top{top:0;left:0;width:100vw;height:300px}.sod-offcanvas .sod-panel.sod-placement-bottom{bottom:0;left:0;width:100vw;height:300px}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-start{animation:sod-slide-in-start .18s ease-out}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-start.is-closing{animation:sod-slide-out-start .18s ease-in forwards}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-end{animation:sod-slide-in-end .18s ease-out}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-end.is-closing{animation:sod-slide-out-end .18s ease-in forwards}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-top{animation:sod-slide-in-top .18s ease-out}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-top.is-closing{animation:sod-slide-out-top .18s ease-in forwards}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-bottom{animation:sod-slide-in-bottom .18s ease-out}.sod-offcanvas .sod-panel.sod-anim-slide.sod-placement-bottom.is-closing{animation:sod-slide-out-bottom .18s ease-in forwards}.sod-offcanvas .sod-panel.sod-anim-fade{animation:sod-fade-in .18s ease-out}.sod-offcanvas .sod-panel.sod-anim-fade.is-closing{animation:sod-fade-out .18s ease-in forwards}.sod-offcanvas .sod-panel.sod-anim-zoom{animation:sod-zoom-in .18s ease-out}.sod-offcanvas .sod-panel.sod-anim-zoom.is-closing{animation:sod-zoom-out .18s ease-in forwards}@keyframes sod-modal-zoom-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@keyframes sod-modal-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.96)}}@keyframes sod-modal-slide-in{0%{opacity:0;transform:translateY(-16px)}to{opacity:1;transform:translateY(0)}}@keyframes sod-modal-slide-out{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-12px)}}@keyframes sod-slide-in-start{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes sod-slide-out-start{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes sod-slide-in-end{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes sod-slide-out-end{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes sod-slide-in-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes sod-slide-out-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes sod-slide-in-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes sod-slide-out-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}@keyframes sod-fade-in{0%{opacity:0}to{opacity:1}}@keyframes sod-fade-out{0%{opacity:1}to{opacity:0}}@keyframes sod-zoom-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@keyframes sod-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.96)}}
@@ -0,0 +1,202 @@
1
+ function A(t, e) {
2
+ if (typeof e == "string") {
3
+ t.innerHTML = e;
4
+ return;
5
+ }
6
+ t.append(e);
7
+ }
8
+ function I(t, e = "hide") {
9
+ if (e === "destroy") {
10
+ t.dataset.sodDestroy = "true", t.open && t.close(), t.remove();
11
+ return;
12
+ }
13
+ t.open && t.close();
14
+ }
15
+ function N(t, e) {
16
+ const i = e ?? "header";
17
+ return i === "panel" ? t : i === "header" ? t.querySelector(".sod-header") : i === "title" ? t.querySelector(".sod-title") : i === "body" ? t.querySelector(".sod-body") : t.querySelector(i) ?? t.querySelector(".sod-header");
18
+ }
19
+ function F(t, e) {
20
+ const i = N(t, e);
21
+ if (!i)
22
+ return;
23
+ t.classList.add("sod-modal-draggable"), i.classList.add("sod-drag-handle");
24
+ let s = !1, H = 0, v = 0, d = null, g = 0, L = 0, o = 0;
25
+ const x = document.body.style.userSelect, c = (r, m) => {
26
+ const a = t.getBoundingClientRect(), M = Math.max(0, window.innerWidth - a.width), w = Math.max(0, window.innerHeight - a.height), p = Math.min(0, window.innerWidth - a.width), l = Math.min(0, window.innerHeight - a.height);
27
+ return {
28
+ left: Math.min(M, Math.max(p, r)),
29
+ top: Math.min(w, Math.max(l, m))
30
+ };
31
+ }, h = (r) => {
32
+ if (!s)
33
+ return;
34
+ const m = c(r.clientX - H, r.clientY - v);
35
+ L = m.left, o = m.top, g === 0 && (g = window.requestAnimationFrame(() => {
36
+ g = 0, t.style.inset = "auto", t.style.left = `${L}px`, t.style.top = `${o}px`, t.style.right = "auto", t.style.bottom = "auto", t.style.margin = "0", t.style.transform = "none";
37
+ }));
38
+ }, u = () => {
39
+ s = !1, i.classList.remove("is-dragging"), t.classList.remove("sod-is-dragging"), t.style.willChange = "", document.body.style.userSelect = x, g !== 0 && (window.cancelAnimationFrame(g), g = 0), d !== null && i.hasPointerCapture && i.hasPointerCapture(d) && i.releasePointerCapture(d), d = null, window.removeEventListener("pointermove", h), window.removeEventListener("pointerup", u), window.removeEventListener("pointercancel", u);
40
+ };
41
+ i.addEventListener("pointerdown", (r) => {
42
+ if (r.button !== 0 || r.target?.closest("button, input, select, textarea, a"))
43
+ return;
44
+ r.preventDefault(), t.classList.add("sod-is-dragging"), t.style.animation = "none", t.style.transition = "none", t.style.transform = "none";
45
+ const a = t.getBoundingClientRect();
46
+ H = r.clientX - a.left, v = r.clientY - a.top, s = !0, d = r.pointerId, i.setPointerCapture(r.pointerId), t.style.position = "fixed", t.style.left = `${a.left}px`, t.style.top = `${a.top}px`, t.style.width = `${Math.round(a.width)}px`, t.style.height = `${Math.round(a.height)}px`, t.style.inset = "auto", t.style.right = "auto", t.style.bottom = "auto", t.style.margin = "0", t.style.willChange = "left, top", i.classList.add("is-dragging"), document.body.style.userSelect = "none", window.addEventListener("pointermove", h), window.addEventListener("pointerup", u), window.addEventListener("pointercancel", u);
47
+ });
48
+ }
49
+ function Y(t, e, i, s, H, v) {
50
+ const g = Math.max(1, v?.hybridSwitchRatio ?? 1.35), L = v?.refitOnContentChange ?? !0, o = Math.max(120, v?.autoFitMinWidth ?? 280), x = Math.max(100, v?.autoFitMinHeight ?? 160), c = v?.scrollMode ? v.scrollMode : v?.autoFitUseScrollbar === !1 ? "viewport" : "body";
51
+ let h = 0;
52
+ const u = (n, f, E) => Math.min(E, Math.max(f, n)), r = () => {
53
+ if (h = 0, !t.isConnected || !t.open)
54
+ return;
55
+ const n = Math.max(o, window.innerWidth - 32), f = Math.max(x, window.innerHeight - 32);
56
+ e.style.width = "auto", e.style.height = "auto", e.style.maxHeight = "none", s.style.maxHeight = "none", s.style.overflowY = "visible", s.style.overflowX = "visible";
57
+ const E = Math.ceil(e.scrollWidth), $ = Math.ceil(e.scrollHeight), W = E > n, S = $ > f;
58
+ if (c === "body") {
59
+ const b = u(E, o, n), y = u($, x, f);
60
+ e.style.width = `${b}px`, e.style.maxWidth = `${n}px`, e.style.height = `${y}px`, e.style.maxHeight = `${f}px`;
61
+ const C = i.offsetHeight, R = H.offsetHeight, D = Math.max(64, y - C - R);
62
+ S ? (s.style.maxHeight = `${D}px`, s.style.overflowY = "auto") : (s.style.maxHeight = "none", s.style.overflowY = "hidden"), s.style.overflowX = W ? "auto" : "hidden", t.classList.remove("sod-modal-viewport-scroll");
63
+ } else if (c === "hybrid")
64
+ if ($ > f * g) {
65
+ const y = Math.max(E, o), C = Math.max($, x);
66
+ e.style.width = `${y}px`, e.style.maxWidth = "none", e.style.height = `${C}px`, e.style.maxHeight = "none", s.style.maxHeight = "none", s.style.overflowY = "visible", s.style.overflowX = "visible", t.classList.add("sod-modal-viewport-scroll");
67
+ } else {
68
+ const y = u(E, o, n), C = u($, x, f);
69
+ e.style.width = `${y}px`, e.style.maxWidth = `${n}px`, e.style.height = `${C}px`, e.style.maxHeight = `${f}px`;
70
+ const R = i.offsetHeight, D = H.offsetHeight, P = Math.max(64, C - R - D);
71
+ S ? (s.style.maxHeight = `${P}px`, s.style.overflowY = "auto") : (s.style.maxHeight = "none", s.style.overflowY = "hidden"), s.style.overflowX = W ? "auto" : "hidden", t.classList.remove("sod-modal-viewport-scroll");
72
+ }
73
+ else if (c === "viewport") {
74
+ const b = Math.max(E, o), y = Math.max($, x);
75
+ e.style.width = `${b}px`, e.style.maxWidth = "none", e.style.height = `${y}px`, e.style.maxHeight = "none", s.style.maxHeight = "none", s.style.overflowY = "visible", s.style.overflowX = "visible", y > f ? t.classList.add("sod-modal-viewport-scroll") : t.classList.remove("sod-modal-viewport-scroll");
76
+ } else {
77
+ const b = W ? Math.max(E, o) : u(E, o, n);
78
+ if (W || S) {
79
+ const y = Math.max($, x);
80
+ e.style.width = `${b}px`, e.style.maxWidth = "none", e.style.height = `${y}px`, e.style.maxHeight = "none", s.style.maxHeight = "none", s.style.overflowY = "visible", s.style.overflowX = "visible", t.classList.add("sod-modal-viewport-scroll");
81
+ } else
82
+ e.style.width = `${b}px`, e.style.maxWidth = `${n}px`, e.style.height = "auto", e.style.maxHeight = "none", s.style.maxHeight = "none", s.style.overflowY = "visible", s.style.overflowX = "visible", t.classList.remove("sod-modal-viewport-scroll");
83
+ }
84
+ }, m = () => {
85
+ h === 0 && (h = window.requestAnimationFrame(r));
86
+ }, a = new ResizeObserver(() => {
87
+ m();
88
+ }), M = new MutationObserver(() => {
89
+ m();
90
+ }), w = () => {
91
+ m();
92
+ }, p = () => {
93
+ m();
94
+ };
95
+ t.addEventListener("sod:refit", p), a.observe(s), a.observe(i), a.observe(H), L && (M.observe(s, {
96
+ subtree: !0,
97
+ childList: !0,
98
+ characterData: !0,
99
+ attributes: !0
100
+ }), s.addEventListener("load", w, !0), s.addEventListener("error", w, !0));
101
+ const l = () => {
102
+ m();
103
+ };
104
+ return window.addEventListener("resize", l), m(), () => {
105
+ h !== 0 && (window.cancelAnimationFrame(h), h = 0), a.disconnect(), M.disconnect(), window.removeEventListener("resize", l), t.removeEventListener("sod:refit", p), s.removeEventListener("load", w, !0), s.removeEventListener("error", w, !0), t.classList.remove("sod-modal-viewport-scroll"), s.style.maxHeight = "", s.style.overflowY = "", s.style.overflowX = "";
106
+ };
107
+ }
108
+ class O {
109
+ static modalRegistry = /* @__PURE__ */ new Map();
110
+ static modalIdSeed = 0;
111
+ static createAutoModalId() {
112
+ return this.modalIdSeed += 1, `sod-modal-${this.modalIdSeed}`;
113
+ }
114
+ static revealExisting(e, i) {
115
+ if (!e.isConnected)
116
+ return;
117
+ const s = e.querySelector(".sod-panel");
118
+ s?.classList.remove("is-closing"), document.body.append(e), e.open || (i ? e.showModal() : e.show()), e.dispatchEvent(new Event("sod:refit")), s?.focus();
119
+ }
120
+ static open(e) {
121
+ const i = e.kind ?? "modal", s = "useModal" in e ? e.useModal ?? !0 : !0, H = i === "modal" ? e : void 0, v = i === "modal" && ("autoFitSize" in e ? e.autoFitSize !== !1 : !0);
122
+ let d, g = !1, L = e.confirmAction ?? "hide";
123
+ if (i === "modal") {
124
+ const l = "id" in e && e.id?.trim() ? e.id.trim() : void 0;
125
+ if (g = !!l, d = l ?? this.createAutoModalId(), e.confirmAction === void 0 && g && (L = "destroy"), l) {
126
+ const n = this.modalRegistry.get(d);
127
+ if (n && n.isConnected) {
128
+ this.revealExisting(n, s);
129
+ const f = {
130
+ dialog: n,
131
+ close: () => I(n),
132
+ refit: () => n.dispatchEvent(new Event("sod:refit")),
133
+ id: d
134
+ };
135
+ return "onReused" in e && e.onReused?.(f), f;
136
+ }
137
+ }
138
+ this.modalRegistry.delete(d);
139
+ }
140
+ const o = document.createElement("dialog");
141
+ o.className = `sod-dialog sod-${i}`;
142
+ const x = [];
143
+ d && (o.dataset.sodId = d, g && (o.dataset.sodPersistent = "true"));
144
+ const c = document.createElement("section");
145
+ if (c.className = "sod-panel", c.tabIndex = -1, i === "offcanvas") {
146
+ const l = "placement" in e ? e.placement ?? "end" : "end", n = "animation" in e ? e.animation ?? "slide" : "slide";
147
+ c.classList.add(`sod-placement-${l}`), c.classList.add(`sod-anim-${n}`);
148
+ } else {
149
+ const l = "position" in e ? e.position ?? "center" : "center", n = "animation" in e ? e.animation ?? "fade" : "fade";
150
+ c.classList.add(`sod-modal-pos-${l}`), c.classList.add(`sod-modal-anim-${n}`), v && c.classList.add("sod-modal-autofit");
151
+ }
152
+ const h = document.createElement("header");
153
+ h.className = "sod-header";
154
+ const u = document.createElement("h2");
155
+ u.className = "sod-title", u.textContent = e.title;
156
+ const r = document.createElement("button");
157
+ r.type = "button", r.className = "sod-close", r.setAttribute("aria-label", "Close"), r.textContent = "×", r.addEventListener("click", () => I(o)), h.append(u, r);
158
+ const m = document.createElement("div");
159
+ m.className = "sod-body", A(m, e.content);
160
+ const a = document.createElement("footer");
161
+ a.className = "sod-footer";
162
+ const M = document.createElement("button");
163
+ M.type = "button", M.className = "sod-btn sod-btn-outline", M.textContent = e.cancelText ?? "取消", M.addEventListener("click", () => {
164
+ e.onCancel?.(), I(o);
165
+ });
166
+ const w = document.createElement("button");
167
+ w.type = "button", w.className = "sod-btn sod-btn-primary", w.textContent = e.confirmText ?? "确认", w.addEventListener("click", () => {
168
+ e.onConfirm?.(), I(o, L);
169
+ }), a.append(M, w), c.append(h, m, a), v && x.push(Y(o, c, h, m, a, H)), i === "modal" && "draggable" in e && e.draggable && F(c, e.dragHandle), o.append(c), (e.closeOnBackdrop ?? !0) && o.addEventListener("click", (l) => {
170
+ l.target === o && I(o);
171
+ }), (e.closeOnEsc ?? !0) || o.addEventListener("cancel", (l) => {
172
+ l.preventDefault();
173
+ }), o.addEventListener("close", () => {
174
+ const l = o.dataset.sodPersistent === "true", n = o.dataset.sodDestroy === "true";
175
+ (!l || n) && (x.forEach((f) => f()), o.remove(), d && this.modalRegistry.delete(d), delete o.dataset.sodDestroy);
176
+ }), document.body.append(o), s ? o.showModal() : o.show(), o.dispatchEvent(new Event("sod:refit")), d && this.modalRegistry.set(d, o);
177
+ const p = {
178
+ dialog: o,
179
+ close: () => I(o),
180
+ refit: () => o.dispatchEvent(new Event("sod:refit")),
181
+ id: d
182
+ };
183
+ return i === "modal" && "onCreated" in e && e.onCreated?.(p), p;
184
+ }
185
+ static openModal(e) {
186
+ return this.open({ ...e, kind: "modal" });
187
+ }
188
+ static openOffcanvas(e) {
189
+ return this.open({ ...e, kind: "offcanvas" });
190
+ }
191
+ }
192
+ function k(t) {
193
+ return O.openModal(t);
194
+ }
195
+ function T(t) {
196
+ return O.openOffcanvas(t);
197
+ }
198
+ export {
199
+ O as SoDialog,
200
+ k as openModal,
201
+ T as openOffcanvas
202
+ };
@@ -0,0 +1 @@
1
+ (function(H,W){typeof exports=="object"&&typeof module<"u"?W(exports):typeof define=="function"&&define.amd?define(["exports"],W):(H=typeof globalThis<"u"?globalThis:H||self,W(H.SoDialog={}))})(this,(function(H){"use strict";function W(t,e){if(typeof e=="string"){t.innerHTML=e;return}t.append(e)}function I(t,e="hide"){if(e==="destroy"){t.dataset.sodDestroy="true",t.open&&t.close(),t.remove();return}t.open&&t.close()}function N(t,e){const i=e??"header";return i==="panel"?t:i==="header"?t.querySelector(".sod-header"):i==="title"?t.querySelector(".sod-title"):i==="body"?t.querySelector(".sod-body"):t.querySelector(i)??t.querySelector(".sod-header")}function T(t,e){const i=N(t,e);if(!i)return;t.classList.add("sod-modal-draggable"),i.classList.add("sod-drag-handle");let s=!1,L=0,v=0,d=null,g=0,p=0,o=0;const y=document.body.style.userSelect,c=(r,m)=>{const a=t.getBoundingClientRect(),M=Math.max(0,window.innerWidth-a.width),w=Math.max(0,window.innerHeight-a.height),$=Math.min(0,window.innerWidth-a.width),l=Math.min(0,window.innerHeight-a.height);return{left:Math.min(M,Math.max($,r)),top:Math.min(w,Math.max(l,m))}},h=r=>{if(!s)return;const m=c(r.clientX-L,r.clientY-v);p=m.left,o=m.top,g===0&&(g=window.requestAnimationFrame(()=>{g=0,t.style.inset="auto",t.style.left=`${p}px`,t.style.top=`${o}px`,t.style.right="auto",t.style.bottom="auto",t.style.margin="0",t.style.transform="none"}))},f=()=>{s=!1,i.classList.remove("is-dragging"),t.classList.remove("sod-is-dragging"),t.style.willChange="",document.body.style.userSelect=y,g!==0&&(window.cancelAnimationFrame(g),g=0),d!==null&&i.hasPointerCapture&&i.hasPointerCapture(d)&&i.releasePointerCapture(d),d=null,window.removeEventListener("pointermove",h),window.removeEventListener("pointerup",f),window.removeEventListener("pointercancel",f)};i.addEventListener("pointerdown",r=>{if(r.button!==0||r.target?.closest("button, input, select, textarea, a"))return;r.preventDefault(),t.classList.add("sod-is-dragging"),t.style.animation="none",t.style.transition="none",t.style.transform="none";const a=t.getBoundingClientRect();L=r.clientX-a.left,v=r.clientY-a.top,s=!0,d=r.pointerId,i.setPointerCapture(r.pointerId),t.style.position="fixed",t.style.left=`${a.left}px`,t.style.top=`${a.top}px`,t.style.width=`${Math.round(a.width)}px`,t.style.height=`${Math.round(a.height)}px`,t.style.inset="auto",t.style.right="auto",t.style.bottom="auto",t.style.margin="0",t.style.willChange="left, top",i.classList.add("is-dragging"),document.body.style.userSelect="none",window.addEventListener("pointermove",h),window.addEventListener("pointerup",f),window.addEventListener("pointercancel",f)})}function F(t,e,i,s,L,v){const g=Math.max(1,v?.hybridSwitchRatio??1.35),p=v?.refitOnContentChange??!0,o=Math.max(120,v?.autoFitMinWidth??280),y=Math.max(100,v?.autoFitMinHeight??160),c=v?.scrollMode?v.scrollMode:v?.autoFitUseScrollbar===!1?"viewport":"body";let h=0;const f=(n,u,E)=>Math.min(E,Math.max(u,n)),r=()=>{if(h=0,!t.isConnected||!t.open)return;const n=Math.max(o,window.innerWidth-32),u=Math.max(y,window.innerHeight-32);e.style.width="auto",e.style.height="auto",e.style.maxHeight="none",s.style.maxHeight="none",s.style.overflowY="visible",s.style.overflowX="visible";const E=Math.ceil(e.scrollWidth),b=Math.ceil(e.scrollHeight),D=E>n,R=b>u;if(c==="body"){const C=f(E,o,n),x=f(b,y,u);e.style.width=`${C}px`,e.style.maxWidth=`${n}px`,e.style.height=`${x}px`,e.style.maxHeight=`${u}px`;const S=i.offsetHeight,P=L.offsetHeight,A=Math.max(64,x-S-P);R?(s.style.maxHeight=`${A}px`,s.style.overflowY="auto"):(s.style.maxHeight="none",s.style.overflowY="hidden"),s.style.overflowX=D?"auto":"hidden",t.classList.remove("sod-modal-viewport-scroll")}else if(c==="hybrid")if(b>u*g){const x=Math.max(E,o),S=Math.max(b,y);e.style.width=`${x}px`,e.style.maxWidth="none",e.style.height=`${S}px`,e.style.maxHeight="none",s.style.maxHeight="none",s.style.overflowY="visible",s.style.overflowX="visible",t.classList.add("sod-modal-viewport-scroll")}else{const x=f(E,o,n),S=f(b,y,u);e.style.width=`${x}px`,e.style.maxWidth=`${n}px`,e.style.height=`${S}px`,e.style.maxHeight=`${u}px`;const P=i.offsetHeight,A=L.offsetHeight,X=Math.max(64,S-P-A);R?(s.style.maxHeight=`${X}px`,s.style.overflowY="auto"):(s.style.maxHeight="none",s.style.overflowY="hidden"),s.style.overflowX=D?"auto":"hidden",t.classList.remove("sod-modal-viewport-scroll")}else if(c==="viewport"){const C=Math.max(E,o),x=Math.max(b,y);e.style.width=`${C}px`,e.style.maxWidth="none",e.style.height=`${x}px`,e.style.maxHeight="none",s.style.maxHeight="none",s.style.overflowY="visible",s.style.overflowX="visible",x>u?t.classList.add("sod-modal-viewport-scroll"):t.classList.remove("sod-modal-viewport-scroll")}else{const C=D?Math.max(E,o):f(E,o,n);if(D||R){const x=Math.max(b,y);e.style.width=`${C}px`,e.style.maxWidth="none",e.style.height=`${x}px`,e.style.maxHeight="none",s.style.maxHeight="none",s.style.overflowY="visible",s.style.overflowX="visible",t.classList.add("sod-modal-viewport-scroll")}else e.style.width=`${C}px`,e.style.maxWidth=`${n}px`,e.style.height="auto",e.style.maxHeight="none",s.style.maxHeight="none",s.style.overflowY="visible",s.style.overflowX="visible",t.classList.remove("sod-modal-viewport-scroll")}},m=()=>{h===0&&(h=window.requestAnimationFrame(r))},a=new ResizeObserver(()=>{m()}),M=new MutationObserver(()=>{m()}),w=()=>{m()},$=()=>{m()};t.addEventListener("sod:refit",$),a.observe(s),a.observe(i),a.observe(L),p&&(M.observe(s,{subtree:!0,childList:!0,characterData:!0,attributes:!0}),s.addEventListener("load",w,!0),s.addEventListener("error",w,!0));const l=()=>{m()};return window.addEventListener("resize",l),m(),()=>{h!==0&&(window.cancelAnimationFrame(h),h=0),a.disconnect(),M.disconnect(),window.removeEventListener("resize",l),t.removeEventListener("sod:refit",$),s.removeEventListener("load",w,!0),s.removeEventListener("error",w,!0),t.classList.remove("sod-modal-viewport-scroll"),s.style.maxHeight="",s.style.overflowY="",s.style.overflowX=""}}class O{static modalRegistry=new Map;static modalIdSeed=0;static createAutoModalId(){return this.modalIdSeed+=1,`sod-modal-${this.modalIdSeed}`}static revealExisting(e,i){if(!e.isConnected)return;const s=e.querySelector(".sod-panel");s?.classList.remove("is-closing"),document.body.append(e),e.open||(i?e.showModal():e.show()),e.dispatchEvent(new Event("sod:refit")),s?.focus()}static open(e){const i=e.kind??"modal",s="useModal"in e?e.useModal??!0:!0,L=i==="modal"?e:void 0,v=i==="modal"&&("autoFitSize"in e?e.autoFitSize!==!1:!0);let d,g=!1,p=e.confirmAction??"hide";if(i==="modal"){const l="id"in e&&e.id?.trim()?e.id.trim():void 0;if(g=!!l,d=l??this.createAutoModalId(),e.confirmAction===void 0&&g&&(p="destroy"),l){const n=this.modalRegistry.get(d);if(n&&n.isConnected){this.revealExisting(n,s);const u={dialog:n,close:()=>I(n),refit:()=>n.dispatchEvent(new Event("sod:refit")),id:d};return"onReused"in e&&e.onReused?.(u),u}}this.modalRegistry.delete(d)}const o=document.createElement("dialog");o.className=`sod-dialog sod-${i}`;const y=[];d&&(o.dataset.sodId=d,g&&(o.dataset.sodPersistent="true"));const c=document.createElement("section");if(c.className="sod-panel",c.tabIndex=-1,i==="offcanvas"){const l="placement"in e?e.placement??"end":"end",n="animation"in e?e.animation??"slide":"slide";c.classList.add(`sod-placement-${l}`),c.classList.add(`sod-anim-${n}`)}else{const l="position"in e?e.position??"center":"center",n="animation"in e?e.animation??"fade":"fade";c.classList.add(`sod-modal-pos-${l}`),c.classList.add(`sod-modal-anim-${n}`),v&&c.classList.add("sod-modal-autofit")}const h=document.createElement("header");h.className="sod-header";const f=document.createElement("h2");f.className="sod-title",f.textContent=e.title;const r=document.createElement("button");r.type="button",r.className="sod-close",r.setAttribute("aria-label","Close"),r.textContent="×",r.addEventListener("click",()=>I(o)),h.append(f,r);const m=document.createElement("div");m.className="sod-body",W(m,e.content);const a=document.createElement("footer");a.className="sod-footer";const M=document.createElement("button");M.type="button",M.className="sod-btn sod-btn-outline",M.textContent=e.cancelText??"取消",M.addEventListener("click",()=>{e.onCancel?.(),I(o)});const w=document.createElement("button");w.type="button",w.className="sod-btn sod-btn-primary",w.textContent=e.confirmText??"确认",w.addEventListener("click",()=>{e.onConfirm?.(),I(o,p)}),a.append(M,w),c.append(h,m,a),v&&y.push(F(o,c,h,m,a,L)),i==="modal"&&"draggable"in e&&e.draggable&&T(c,e.dragHandle),o.append(c),(e.closeOnBackdrop??!0)&&o.addEventListener("click",l=>{l.target===o&&I(o)}),(e.closeOnEsc??!0)||o.addEventListener("cancel",l=>{l.preventDefault()}),o.addEventListener("close",()=>{const l=o.dataset.sodPersistent==="true",n=o.dataset.sodDestroy==="true";(!l||n)&&(y.forEach(u=>u()),o.remove(),d&&this.modalRegistry.delete(d),delete o.dataset.sodDestroy)}),document.body.append(o),s?o.showModal():o.show(),o.dispatchEvent(new Event("sod:refit")),d&&this.modalRegistry.set(d,o);const $={dialog:o,close:()=>I(o),refit:()=>o.dispatchEvent(new Event("sod:refit")),id:d};return i==="modal"&&"onCreated"in e&&e.onCreated?.($),$}static openModal(e){return this.open({...e,kind:"modal"})}static openOffcanvas(e){return this.open({...e,kind:"offcanvas"})}}function Y(t){return O.openModal(t)}function k(t){return O.openOffcanvas(t)}H.SoDialog=O,H.openModal=Y,H.openOffcanvas=k,Object.defineProperty(H,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,60 @@
1
+ import './sodialog.css';
2
+ export type SoPanelKind = 'modal' | 'offcanvas';
3
+ export type SoOffcanvasPlacement = 'start' | 'end' | 'top' | 'bottom';
4
+ export type SoOffcanvasAnimation = 'slide' | 'fade' | 'zoom';
5
+ export type SoModalPosition = 'center' | 'top' | 'bottom';
6
+ export type SoModalAnimation = 'slide' | 'fade' | 'zoom';
7
+ export type SoModalDragHandle = 'header' | 'title' | 'body' | 'panel' | string;
8
+ export type SoModalScrollMode = 'body' | 'viewport' | 'none' | 'hybrid';
9
+ export interface SoDialogBaseOptions {
10
+ title: string;
11
+ content: string | Node;
12
+ confirmText?: string;
13
+ cancelText?: string;
14
+ confirmAction?: 'hide' | 'destroy';
15
+ closeOnBackdrop?: boolean;
16
+ closeOnEsc?: boolean;
17
+ onConfirm?: () => void;
18
+ onCancel?: () => void;
19
+ }
20
+ export interface SoDialogModalOptions extends SoDialogBaseOptions {
21
+ id?: string;
22
+ kind?: 'modal';
23
+ position?: SoModalPosition;
24
+ animation?: SoModalAnimation;
25
+ useModal?: boolean;
26
+ draggable?: boolean;
27
+ dragHandle?: SoModalDragHandle;
28
+ autoFitSize?: boolean;
29
+ scrollMode?: SoModalScrollMode;
30
+ hybridSwitchRatio?: number;
31
+ autoFitUseScrollbar?: boolean;
32
+ refitOnContentChange?: boolean;
33
+ autoFitMinWidth?: number;
34
+ autoFitMinHeight?: number;
35
+ onCreated?: (handle: SoDialogHandle) => void;
36
+ onReused?: (handle: SoDialogHandle) => void;
37
+ }
38
+ export interface SoDialogOffcanvasOptions extends SoDialogBaseOptions {
39
+ kind: 'offcanvas';
40
+ placement?: SoOffcanvasPlacement;
41
+ animation?: SoOffcanvasAnimation;
42
+ }
43
+ export type SoDialogOptions = SoDialogModalOptions | SoDialogOffcanvasOptions;
44
+ export interface SoDialogHandle {
45
+ dialog: HTMLDialogElement;
46
+ close: () => void;
47
+ refit: () => void;
48
+ id?: string;
49
+ }
50
+ export declare class SoDialog {
51
+ private static modalRegistry;
52
+ private static modalIdSeed;
53
+ private static createAutoModalId;
54
+ private static revealExisting;
55
+ static open(options: SoDialogOptions): SoDialogHandle;
56
+ static openModal(options: SoDialogModalOptions): SoDialogHandle;
57
+ static openOffcanvas(options: Omit<SoDialogOffcanvasOptions, 'kind'>): SoDialogHandle;
58
+ }
59
+ export declare function openModal(options: SoDialogModalOptions): SoDialogHandle;
60
+ export declare function openOffcanvas(options: Omit<SoDialogOffcanvasOptions, 'kind'>): SoDialogHandle;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "sodialog",
3
+ "version": "0.1.1",
4
+ "description": "A reusable HTML5 dialog-based modal and offcanvas library.",
5
+ "keywords": [
6
+ "dialog",
7
+ "modal",
8
+ "offcanvas",
9
+ "typescript",
10
+ "html5"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "main": "./dist/sodialog.es.js",
15
+ "module": "./dist/sodialog.es.js",
16
+ "types": "./dist/types/lib.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/types/lib.d.ts",
20
+ "import": "./dist/sodialog.es.js",
21
+ "default": "./dist/sodialog.es.js"
22
+ },
23
+ "./style.css": "./dist/sodialog.css"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "dev": "vite",
30
+ "build": "vite build && npm run build:types",
31
+ "build:types": "tsc -p tsconfig.build.json",
32
+ "build:demo": "vite build --mode demo",
33
+ "preview": "vite preview",
34
+ "lint": "eslint .",
35
+ "prepublishOnly": "npm run lint && npm run build"
36
+ },
37
+ "sideEffects": [
38
+ "**/*.css"
39
+ ],
40
+ "devDependencies": {
41
+ "@eslint/js": "^10.0.1",
42
+ "eslint": "^10.0.2",
43
+ "typescript": "~5.9.3",
44
+ "typescript-eslint": "^8.56.1",
45
+ "vite": "^7.3.1"
46
+ }
47
+ }