@icen.ai/ui 0.4.0 → 0.5.0
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/dist/behaviors/back-top.mjs +84 -0
- package/dist/behaviors/command-palette.mjs +177 -0
- package/dist/behaviors/date-picker.mjs +228 -0
- package/dist/behaviors/input.mjs +170 -1
- package/dist/behaviors/notification.mjs +754 -0
- package/dist/behaviors/select.mjs +151 -22
- package/dist/behaviors/split-pane.mjs +102 -0
- package/dist/behaviors/upload.mjs +165 -14
- package/dist/components/back-top.css +88 -0
- package/dist/components/back-top.mjs +3 -0
- package/dist/components/badge.css +131 -0
- package/dist/components/badge.mjs +2 -0
- package/dist/components/breadcrumb.css +57 -0
- package/dist/components/card.css +68 -0
- package/dist/components/command-palette.css +246 -0
- package/dist/components/command-palette.mjs +3 -0
- package/dist/components/content.css +214 -0
- package/dist/components/copy.css +93 -0
- package/dist/components/copy.mjs +3 -0
- package/dist/components/date-picker.css +244 -0
- package/dist/components/date-picker.mjs +3 -0
- package/dist/components/empty.css +73 -3
- package/dist/components/feedback.css +239 -1
- package/dist/components/form.css +119 -0
- package/dist/components/form.mjs +2 -0
- package/dist/components/input.css +24 -1
- package/dist/components/media.css +144 -1
- package/dist/components/nav.css +99 -0
- package/dist/components/notification.css +387 -0
- package/dist/components/notification.mjs +3 -0
- package/dist/components/panel.css +100 -5
- package/dist/components/pill.css +70 -0
- package/dist/components/scroll-area.css +112 -0
- package/dist/components/scroll-area.mjs +2 -0
- package/dist/components/segmented.css +75 -0
- package/dist/components/select.css +97 -2
- package/dist/components/sidebar.css +118 -0
- package/dist/components/slider.css +113 -0
- package/dist/components/split-pane.css +112 -0
- package/dist/components/split-pane.mjs +3 -0
- package/dist/components/stat.css +129 -6
- package/dist/components/steps.css +97 -0
- package/dist/components/switch.css +142 -0
- package/dist/components/table.css +86 -3
- package/dist/components/tabs.css +122 -1
- package/dist/components/tag.css +164 -0
- package/dist/components/tag.mjs +2 -0
- package/dist/components/toast.css +114 -0
- package/dist/components/toolbar.css +101 -0
- package/dist/components/toolbar.mjs +2 -0
- package/dist/components/upload.css +61 -4
- package/dist/index.mjs +1858 -69
- package/dist/registry.json +64 -1
- package/dist/ui.css +4320 -278
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/behaviors/back-top.ts
|
|
2
|
+
var AUTO_BTN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';
|
|
3
|
+
function prefersReducedMotion() {
|
|
4
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
5
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6
|
+
}
|
|
7
|
+
function getScrollTarget(target) {
|
|
8
|
+
if (!target) return { el: null, isWindow: true };
|
|
9
|
+
const el = document.querySelector(target);
|
|
10
|
+
return { el, isWindow: false };
|
|
11
|
+
}
|
|
12
|
+
function getScrollY(target) {
|
|
13
|
+
if (!target) return window.scrollY;
|
|
14
|
+
const el = document.querySelector(target);
|
|
15
|
+
return el ? el.scrollTop : window.scrollY;
|
|
16
|
+
}
|
|
17
|
+
function scrollToTop(target, offset = 0) {
|
|
18
|
+
const reduce = prefersReducedMotion();
|
|
19
|
+
const behavior = reduce ? "auto" : "smooth";
|
|
20
|
+
if (!target) {
|
|
21
|
+
window.scrollTo({ top: offset, behavior });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const el = document.querySelector(target);
|
|
25
|
+
if (el) el.scrollTo({ top: offset, behavior });
|
|
26
|
+
}
|
|
27
|
+
function setup(btn, opts = {}) {
|
|
28
|
+
const el = btn;
|
|
29
|
+
if (el.__icenBackTopInit) return;
|
|
30
|
+
el.__icenBackTopInit = true;
|
|
31
|
+
const threshold = Number(btn.dataset.backTopThreshold ?? opts.threshold ?? 320) || 320;
|
|
32
|
+
const target = btn.dataset.backTopTarget ?? opts.target;
|
|
33
|
+
const offset = Number(btn.dataset.backTopOffset ?? opts.offset ?? 0) || 0;
|
|
34
|
+
let ticking = false;
|
|
35
|
+
const apply = () => {
|
|
36
|
+
ticking = false;
|
|
37
|
+
const y = getScrollY(target);
|
|
38
|
+
btn.classList.toggle("is-shown", y > threshold);
|
|
39
|
+
};
|
|
40
|
+
const onScroll = () => {
|
|
41
|
+
if (ticking) return;
|
|
42
|
+
ticking = true;
|
|
43
|
+
window.requestAnimationFrame(apply);
|
|
44
|
+
};
|
|
45
|
+
const { el: targetEl, isWindow } = getScrollTarget(target);
|
|
46
|
+
if (isWindow) {
|
|
47
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
48
|
+
} else if (targetEl) {
|
|
49
|
+
targetEl.addEventListener("scroll", onScroll, { passive: true });
|
|
50
|
+
}
|
|
51
|
+
window.addEventListener("resize", onScroll, { passive: true });
|
|
52
|
+
btn.addEventListener("click", () => scrollToTop(target, offset));
|
|
53
|
+
apply();
|
|
54
|
+
}
|
|
55
|
+
function initBackTop(opts = {}) {
|
|
56
|
+
if (typeof document === "undefined") return;
|
|
57
|
+
if (typeof window === "undefined") return;
|
|
58
|
+
const root = opts.root ?? document;
|
|
59
|
+
if (opts.autoCreate) {
|
|
60
|
+
let btn = document.body.querySelector(".back-top[data-back-top-auto]");
|
|
61
|
+
if (!btn) {
|
|
62
|
+
btn = document.createElement("button");
|
|
63
|
+
btn.type = "button";
|
|
64
|
+
btn.className = "back-top";
|
|
65
|
+
btn.setAttribute("data-back-top", "");
|
|
66
|
+
btn.setAttribute("data-back-top-auto", "");
|
|
67
|
+
btn.setAttribute("aria-label", "\u56DE\u5230\u9876\u90E8");
|
|
68
|
+
btn.innerHTML = AUTO_BTN_SVG;
|
|
69
|
+
document.body.appendChild(btn);
|
|
70
|
+
}
|
|
71
|
+
setup(btn, opts);
|
|
72
|
+
}
|
|
73
|
+
const candidates = [];
|
|
74
|
+
if (root instanceof Element && root.matches(".back-top[data-back-top]")) candidates.push(root);
|
|
75
|
+
candidates.push(...Array.from(root.querySelectorAll(".back-top[data-back-top]")));
|
|
76
|
+
for (const c of candidates) {
|
|
77
|
+
const el = c;
|
|
78
|
+
if (el.hasAttribute("data-back-top-auto") && opts.autoCreate) continue;
|
|
79
|
+
setup(el, opts);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
initBackTop
|
|
84
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/behaviors/command-palette.ts
|
|
2
|
+
var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
3
|
+
var openId = null;
|
|
4
|
+
var restoreFocusTo = null;
|
|
5
|
+
var prevBodyOverflow = "";
|
|
6
|
+
var keyHandler = null;
|
|
7
|
+
var globalKeyHandler = null;
|
|
8
|
+
var initialized = false;
|
|
9
|
+
function findBackdrop(id) {
|
|
10
|
+
return document.querySelector(
|
|
11
|
+
`.command-palette-backdrop[data-command-palette="${CSS.escape(id)}"]`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function getVisibleItems(panel) {
|
|
15
|
+
return Array.from(
|
|
16
|
+
panel.querySelectorAll(".command-palette-item:not([hidden]):not([disabled])")
|
|
17
|
+
).filter((el) => el.offsetParent !== null || el.getClientRects().length > 0);
|
|
18
|
+
}
|
|
19
|
+
function isDisabledItem(el) {
|
|
20
|
+
return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
21
|
+
}
|
|
22
|
+
function setActive(panel, idx) {
|
|
23
|
+
const items = getVisibleItems(panel);
|
|
24
|
+
items.forEach((el, i) => el.classList.toggle("is-active", i === idx));
|
|
25
|
+
const active = items[idx];
|
|
26
|
+
if (active && typeof active.scrollIntoView === "function") {
|
|
27
|
+
active.scrollIntoView({ block: "nearest" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function filterPanel(panel, query) {
|
|
31
|
+
const q = query.trim().toLowerCase();
|
|
32
|
+
let visibleCount = 0;
|
|
33
|
+
panel.querySelectorAll(".command-palette-item").forEach((item) => {
|
|
34
|
+
if (isDisabledItem(item)) return;
|
|
35
|
+
const label = (item.querySelector(".command-palette-item-label")?.textContent ?? "").toLowerCase();
|
|
36
|
+
const hint = (item.querySelector(".command-palette-item-hint")?.textContent ?? "").toLowerCase();
|
|
37
|
+
const keyword = (item.dataset.commandPaletteKeyword ?? "").toLowerCase();
|
|
38
|
+
const match = !q || label.includes(q) || hint.includes(q) || keyword.includes(q);
|
|
39
|
+
item.hidden = !match;
|
|
40
|
+
item.classList.remove("is-active");
|
|
41
|
+
if (match) visibleCount++;
|
|
42
|
+
});
|
|
43
|
+
panel.querySelectorAll(".command-palette-group").forEach((group) => {
|
|
44
|
+
const hasVisible = Array.from(group.querySelectorAll(".command-palette-item")).some(
|
|
45
|
+
(it) => !it.hidden
|
|
46
|
+
);
|
|
47
|
+
group.hidden = !hasVisible;
|
|
48
|
+
});
|
|
49
|
+
const empty = panel.querySelector(".command-palette-empty");
|
|
50
|
+
if (empty) empty.hidden = visibleCount !== 0;
|
|
51
|
+
if (visibleCount > 0) setActive(panel, 0);
|
|
52
|
+
}
|
|
53
|
+
function openCommandPalette(id) {
|
|
54
|
+
if (typeof document === "undefined") return;
|
|
55
|
+
if (openId === id) return;
|
|
56
|
+
if (openId) closeCommandPalette(openId);
|
|
57
|
+
const backdrop = findBackdrop(id);
|
|
58
|
+
if (!backdrop) return;
|
|
59
|
+
const panel = backdrop.querySelector(".command-palette");
|
|
60
|
+
if (!panel) return;
|
|
61
|
+
if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
|
|
62
|
+
restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
63
|
+
openId = id;
|
|
64
|
+
backdrop.hidden = false;
|
|
65
|
+
prevBodyOverflow = document.body.style.overflow;
|
|
66
|
+
document.body.style.overflow = "hidden";
|
|
67
|
+
const input = panel.querySelector(".command-palette-input");
|
|
68
|
+
if (input) {
|
|
69
|
+
input.value = "";
|
|
70
|
+
input.focus();
|
|
71
|
+
} else {
|
|
72
|
+
panel.focus();
|
|
73
|
+
}
|
|
74
|
+
filterPanel(panel, "");
|
|
75
|
+
keyHandler = (e) => {
|
|
76
|
+
if (e.key === "Escape") {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
closeCommandPalette(id);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const items = getVisibleItems(panel);
|
|
84
|
+
if (items.length === 0) return;
|
|
85
|
+
const cur = items.findIndex((el) => el.classList.contains("is-active"));
|
|
86
|
+
const next = e.key === "ArrowDown" ? (cur + 1) % items.length : (cur - 1 + items.length) % items.length;
|
|
87
|
+
setActive(panel, next);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (e.key === "Enter") {
|
|
91
|
+
const active = panel.querySelector(".command-palette-item.is-active");
|
|
92
|
+
if (active && !isDisabledItem(active)) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
active.click();
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (e.key === "Tab") {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
const els = Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
|
|
101
|
+
(el) => !el.hidden && el.offsetParent !== null
|
|
102
|
+
);
|
|
103
|
+
if (els.length === 0) return;
|
|
104
|
+
const cur = els.findIndex((el) => el === document.activeElement);
|
|
105
|
+
const next = e.shiftKey ? (cur - 1 + els.length) % els.length : (cur + 1) % els.length;
|
|
106
|
+
els[next]?.focus();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
window.addEventListener("keydown", keyHandler);
|
|
110
|
+
}
|
|
111
|
+
function closeCommandPalette(id) {
|
|
112
|
+
if (typeof document === "undefined") return;
|
|
113
|
+
const backdrop = findBackdrop(id);
|
|
114
|
+
if (!backdrop) return;
|
|
115
|
+
backdrop.hidden = true;
|
|
116
|
+
if (openId !== id) return;
|
|
117
|
+
openId = null;
|
|
118
|
+
if (keyHandler) {
|
|
119
|
+
window.removeEventListener("keydown", keyHandler);
|
|
120
|
+
keyHandler = null;
|
|
121
|
+
}
|
|
122
|
+
document.body.style.overflow = prevBodyOverflow;
|
|
123
|
+
if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
|
|
124
|
+
restoreFocusTo = null;
|
|
125
|
+
}
|
|
126
|
+
function initCommandPalette(root = document) {
|
|
127
|
+
if (typeof document === "undefined") return;
|
|
128
|
+
if (typeof window === "undefined") return;
|
|
129
|
+
root.querySelectorAll("[data-command-palette-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
|
|
130
|
+
if (initialized) return;
|
|
131
|
+
initialized = true;
|
|
132
|
+
document.addEventListener("click", (e) => {
|
|
133
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
134
|
+
if (!target) return;
|
|
135
|
+
const opener = target.closest("[data-command-palette-open]");
|
|
136
|
+
if (opener?.dataset.commandPaletteOpen) {
|
|
137
|
+
openCommandPalette(opener.dataset.commandPaletteOpen);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const item = target.closest(".command-palette-item");
|
|
141
|
+
if (item) {
|
|
142
|
+
const backdrop = item.closest("[data-command-palette]");
|
|
143
|
+
if (backdrop?.dataset.commandPalette) {
|
|
144
|
+
if (!item.hasAttribute("data-command-palette-keep-open")) {
|
|
145
|
+
closeCommandPalette(backdrop.dataset.commandPalette);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (target instanceof HTMLElement && target.classList.contains("command-palette-backdrop") && target.dataset.commandPalette) {
|
|
151
|
+
closeCommandPalette(target.dataset.commandPalette);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
document.addEventListener("input", (e) => {
|
|
155
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
156
|
+
if (!e.target.matches(".command-palette-input")) return;
|
|
157
|
+
const backdrop = e.target.closest("[data-command-palette]");
|
|
158
|
+
const panel = backdrop?.querySelector(".command-palette");
|
|
159
|
+
if (panel) filterPanel(panel, e.target.value);
|
|
160
|
+
});
|
|
161
|
+
globalKeyHandler = (e) => {
|
|
162
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
const first = document.querySelector(".command-palette-backdrop[data-command-palette]");
|
|
165
|
+
if (first?.dataset.commandPalette) {
|
|
166
|
+
if (openId === first.dataset.commandPalette) closeCommandPalette(openId);
|
|
167
|
+
else openCommandPalette(first.dataset.commandPalette);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
window.addEventListener("keydown", globalKeyHandler);
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
closeCommandPalette,
|
|
175
|
+
initCommandPalette,
|
|
176
|
+
openCommandPalette
|
|
177
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// src/behaviors/date-picker.ts
|
|
2
|
+
var openPicker = null;
|
|
3
|
+
var panelEl = null;
|
|
4
|
+
var outsideHandler = null;
|
|
5
|
+
var escHandler = null;
|
|
6
|
+
function pad(n) {
|
|
7
|
+
return n < 10 ? "0" + n : String(n);
|
|
8
|
+
}
|
|
9
|
+
function toDateStr(d) {
|
|
10
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
11
|
+
}
|
|
12
|
+
function parseDateStr(s) {
|
|
13
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.trim());
|
|
14
|
+
if (!m) return null;
|
|
15
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
16
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
17
|
+
}
|
|
18
|
+
function format(d, fmt) {
|
|
19
|
+
if (!d) return "";
|
|
20
|
+
return fmt.replace(/yyyy/g, String(d.getFullYear())).replace(/yy/g, String(d.getFullYear()).slice(-2)).replace(/mm/g, pad(d.getMonth() + 1)).replace(/m/g, String(d.getMonth() + 1)).replace(/dd/g, pad(d.getDate())).replace(/d/g, String(d.getDate()));
|
|
21
|
+
}
|
|
22
|
+
function ensurePanel() {
|
|
23
|
+
if (panelEl && panelEl.isConnected) return panelEl;
|
|
24
|
+
const el = document.createElement("div");
|
|
25
|
+
el.className = "date-picker-panel";
|
|
26
|
+
el.setAttribute("role", "dialog");
|
|
27
|
+
el.setAttribute("aria-label", "\u9009\u62E9\u65E5\u671F");
|
|
28
|
+
el.hidden = true;
|
|
29
|
+
document.body.appendChild(el);
|
|
30
|
+
panelEl = el;
|
|
31
|
+
return el;
|
|
32
|
+
}
|
|
33
|
+
function positionPanel(panel, trigger) {
|
|
34
|
+
const r = trigger.getBoundingClientRect();
|
|
35
|
+
const pw = 280;
|
|
36
|
+
const ph = panel.offsetHeight || 320;
|
|
37
|
+
let left = r.left;
|
|
38
|
+
let top = r.bottom + 4;
|
|
39
|
+
if (left + pw > window.innerWidth - 12) left = window.innerWidth - pw - 12;
|
|
40
|
+
if (left < 12) left = 12;
|
|
41
|
+
if (top + ph > window.innerHeight - 12) top = r.top - ph - 4;
|
|
42
|
+
if (top < 12) top = 12;
|
|
43
|
+
panel.style.left = left + "px";
|
|
44
|
+
panel.style.top = top + "px";
|
|
45
|
+
}
|
|
46
|
+
var CAL_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>';
|
|
47
|
+
var WEEK_LABELS_ZH = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"];
|
|
48
|
+
var WEEK_LABELS_SUN = ["\u65E5", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D"];
|
|
49
|
+
function renderCalendar(panel, ctx) {
|
|
50
|
+
const { view, selected, min, max, weekStart, onPick } = ctx;
|
|
51
|
+
const year = view.getFullYear();
|
|
52
|
+
const month = view.getMonth();
|
|
53
|
+
const labels = weekStart === 0 ? WEEK_LABELS_SUN : WEEK_LABELS_ZH;
|
|
54
|
+
const firstDay = new Date(year, month, 1);
|
|
55
|
+
const offsetRaw = firstDay.getDay();
|
|
56
|
+
const leadDays = weekStart === 0 ? offsetRaw : (offsetRaw + 6) % 7;
|
|
57
|
+
const gridStart = new Date(year, month, 1 - leadDays);
|
|
58
|
+
const today = /* @__PURE__ */ new Date();
|
|
59
|
+
today.setHours(0, 0, 0, 0);
|
|
60
|
+
const cells = [];
|
|
61
|
+
for (let i = 0; i < 42; i++) {
|
|
62
|
+
cells.push(new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i));
|
|
63
|
+
}
|
|
64
|
+
let html = `<div class="date-picker-head">
|
|
65
|
+
<button class="date-picker-nav" data-dp-nav="-1" type="button" aria-label="\u4E0A\u4E00\u6708">\u2039</button>
|
|
66
|
+
<span class="date-picker-title">${year} \u5E74 ${month + 1} \u6708</span>
|
|
67
|
+
<button class="date-picker-nav" data-dp-nav="1" type="button" aria-label="\u4E0B\u4E00\u6708">\u203A</button>
|
|
68
|
+
</div><div class="date-picker-weekdays">${labels.map((l) => `<span>${l}</span>`).join("")}</div><div class="date-picker-grid">`;
|
|
69
|
+
for (const d of cells) {
|
|
70
|
+
const isOutside = d.getMonth() !== month;
|
|
71
|
+
const ds = toDateStr(d);
|
|
72
|
+
const isToday = d.getTime() === today.getTime();
|
|
73
|
+
const isSelected = selected && ds === toDateStr(selected);
|
|
74
|
+
const disabled = min && d < min || max && d > max;
|
|
75
|
+
const classes = ["date-picker-cell"];
|
|
76
|
+
if (isOutside) classes.push("is-outside");
|
|
77
|
+
if (isToday) classes.push("is-today");
|
|
78
|
+
if (isSelected) classes.push("is-selected");
|
|
79
|
+
if (disabled) classes.push("is-disabled");
|
|
80
|
+
html += `<button type="button" class="${classes.join(" ")}" data-dp-date="${ds}"${disabled ? " disabled" : ""}>${d.getDate()}</button>`;
|
|
81
|
+
}
|
|
82
|
+
html += `</div><div class="date-picker-foot">
|
|
83
|
+
<button type="button" class="date-picker-today" data-dp-today>\u4ECA\u5929</button>
|
|
84
|
+
<button type="button" class="date-picker-clear" data-dp-clear>\u6E05\u9664</button>
|
|
85
|
+
</div>`;
|
|
86
|
+
panel.innerHTML = html;
|
|
87
|
+
panel.querySelector('[data-dp-nav="-1"]')?.addEventListener("click", () => {
|
|
88
|
+
ctx.view = new Date(year, month - 1, 15);
|
|
89
|
+
renderCalendar(panel, { ...ctx, view: ctx.view });
|
|
90
|
+
});
|
|
91
|
+
panel.querySelector('[data-dp-nav="1"]')?.addEventListener("click", () => {
|
|
92
|
+
ctx.view = new Date(year, month + 1, 15);
|
|
93
|
+
renderCalendar(panel, { ...ctx, view: ctx.view });
|
|
94
|
+
});
|
|
95
|
+
panel.querySelectorAll("[data-dp-date]").forEach((btn) => {
|
|
96
|
+
btn.addEventListener("click", () => {
|
|
97
|
+
const ds = btn.dataset.dpDate;
|
|
98
|
+
if (!ds) return;
|
|
99
|
+
const d = parseDateStr(ds);
|
|
100
|
+
if (d) onPick(d);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
panel.querySelector("[data-dp-today]")?.addEventListener("click", () => {
|
|
104
|
+
onPick(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
|
|
105
|
+
});
|
|
106
|
+
panel.querySelector("[data-dp-clear]")?.addEventListener("click", () => {
|
|
107
|
+
onPick(null);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function closePanel() {
|
|
111
|
+
if (!panelEl) return;
|
|
112
|
+
panelEl.hidden = true;
|
|
113
|
+
openPicker?.querySelector(".date-picker-trigger")?.classList.remove("is-open");
|
|
114
|
+
openPicker = null;
|
|
115
|
+
if (outsideHandler) {
|
|
116
|
+
document.removeEventListener("mousedown", outsideHandler);
|
|
117
|
+
outsideHandler = null;
|
|
118
|
+
}
|
|
119
|
+
if (escHandler) {
|
|
120
|
+
window.removeEventListener("keydown", escHandler);
|
|
121
|
+
escHandler = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function setupPicker(container) {
|
|
125
|
+
const el = container;
|
|
126
|
+
if (el.__icenDatePickerInit) return;
|
|
127
|
+
el.__icenDatePickerInit = true;
|
|
128
|
+
const fmt = el.dataset.datePickerFormat ?? "yyyy-mm-dd";
|
|
129
|
+
const minStr = el.dataset.datePickerMin;
|
|
130
|
+
const maxStr = el.dataset.datePickerMax;
|
|
131
|
+
const weekStart = Number(el.dataset.datePickerWeekStart ?? "1");
|
|
132
|
+
const placeholder = el.dataset.datePickerPlaceholder ?? "\u9009\u62E9\u65E5\u671F";
|
|
133
|
+
const min = minStr ? parseDateStr(minStr) : null;
|
|
134
|
+
const max = maxStr ? parseDateStr(maxStr) : null;
|
|
135
|
+
const hidden = el.querySelector('input[type="hidden"]');
|
|
136
|
+
const initialStr = hidden?.value ?? "";
|
|
137
|
+
const initialDate = initialStr ? parseDateStr(initialStr) : null;
|
|
138
|
+
let trigger = el.querySelector(".date-picker-trigger");
|
|
139
|
+
if (!trigger) {
|
|
140
|
+
trigger = document.createElement("button");
|
|
141
|
+
trigger.type = "button";
|
|
142
|
+
trigger.className = "date-picker-trigger";
|
|
143
|
+
trigger.innerHTML = `<span class="date-picker-value${initialDate ? "" : " is-empty"}"></span><span class="date-picker-icon">${CAL_ICON}</span>`;
|
|
144
|
+
el.prepend(trigger);
|
|
145
|
+
}
|
|
146
|
+
trigger.setAttribute("aria-haspopup", "dialog");
|
|
147
|
+
const valueEl = trigger.querySelector(".date-picker-value");
|
|
148
|
+
function paint() {
|
|
149
|
+
const v = hidden?.value ?? "";
|
|
150
|
+
const d = v ? parseDateStr(v) : null;
|
|
151
|
+
if (valueEl) {
|
|
152
|
+
valueEl.textContent = d ? format(d, fmt) : placeholder;
|
|
153
|
+
valueEl.classList.toggle("is-empty", !d);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
paint();
|
|
157
|
+
if (hidden) {
|
|
158
|
+
const obs = new MutationObserver(() => paint());
|
|
159
|
+
obs.observe(hidden, { attributes: true, attributeFilter: ["value"] });
|
|
160
|
+
}
|
|
161
|
+
trigger.addEventListener("click", () => {
|
|
162
|
+
if (openPicker === el) {
|
|
163
|
+
closePanel();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
closePanel();
|
|
167
|
+
openPicker = el;
|
|
168
|
+
trigger.classList.add("is-open");
|
|
169
|
+
const panel = ensurePanel();
|
|
170
|
+
panel.hidden = false;
|
|
171
|
+
const selected = hidden?.value ? parseDateStr(hidden.value) : null;
|
|
172
|
+
const view = selected ?? /* @__PURE__ */ new Date();
|
|
173
|
+
view.setDate(15);
|
|
174
|
+
positionPanel(panel, trigger);
|
|
175
|
+
renderCalendar(panel, {
|
|
176
|
+
view,
|
|
177
|
+
selected,
|
|
178
|
+
min,
|
|
179
|
+
max,
|
|
180
|
+
weekStart,
|
|
181
|
+
onPick: (d) => {
|
|
182
|
+
if (d === null) {
|
|
183
|
+
if (hidden) {
|
|
184
|
+
hidden.value = "";
|
|
185
|
+
hidden.dispatchEvent(new Event("change", { bubbles: true }));
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
if (hidden) {
|
|
189
|
+
hidden.value = toDateStr(d);
|
|
190
|
+
hidden.dispatchEvent(new Event("change", { bubbles: true }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
paint();
|
|
194
|
+
closePanel();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
outsideHandler = (ev) => {
|
|
198
|
+
const t = ev.target instanceof Node ? ev.target : null;
|
|
199
|
+
if (!t) return;
|
|
200
|
+
if (panel.contains(t) || el.contains(t)) return;
|
|
201
|
+
closePanel();
|
|
202
|
+
};
|
|
203
|
+
document.addEventListener("mousedown", outsideHandler);
|
|
204
|
+
escHandler = (ev) => {
|
|
205
|
+
if (ev.key === "Escape") {
|
|
206
|
+
ev.preventDefault();
|
|
207
|
+
closePanel();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
window.addEventListener("keydown", escHandler);
|
|
211
|
+
const reposition = () => {
|
|
212
|
+
if (openPicker === el && panel && !panel.hidden) positionPanel(panel, trigger);
|
|
213
|
+
};
|
|
214
|
+
window.addEventListener("scroll", reposition, { passive: true, once: false });
|
|
215
|
+
window.addEventListener("resize", reposition, { passive: true });
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function initDatePicker(root = document) {
|
|
219
|
+
if (typeof document === "undefined") return;
|
|
220
|
+
if (typeof window === "undefined") return;
|
|
221
|
+
const pickers = [];
|
|
222
|
+
if (root instanceof Element && root.matches(".date-picker[data-date-picker]")) pickers.push(root);
|
|
223
|
+
pickers.push(...Array.from(root.querySelectorAll(".date-picker[data-date-picker]")));
|
|
224
|
+
for (const p of pickers) setupPicker(p);
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
initDatePicker
|
|
228
|
+
};
|
package/dist/behaviors/input.mjs
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// src/behaviors/input.ts
|
|
2
|
+
var isComposing = false;
|
|
3
|
+
if (typeof document !== "undefined") {
|
|
4
|
+
document.addEventListener("compositionstart", () => {
|
|
5
|
+
isComposing = true;
|
|
6
|
+
});
|
|
7
|
+
document.addEventListener("compositionend", () => {
|
|
8
|
+
isComposing = false;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
2
11
|
function setupClear(wrap) {
|
|
3
12
|
const btn = wrap.querySelector(".input-clear");
|
|
4
|
-
const field = wrap.querySelector("input.input");
|
|
13
|
+
const field = wrap.querySelector("input.input, textarea.textarea");
|
|
5
14
|
if (!btn || !field) return;
|
|
6
15
|
btn.addEventListener("click", () => {
|
|
7
16
|
field.value = "";
|
|
@@ -43,6 +52,140 @@ function setupAutosize(ta) {
|
|
|
43
52
|
ta.addEventListener("input", () => resizeTextarea(ta));
|
|
44
53
|
resizeTextarea(ta);
|
|
45
54
|
}
|
|
55
|
+
function setupCounter(field) {
|
|
56
|
+
const maxAttr = field.getAttribute("maxlength");
|
|
57
|
+
const dataCount = field.getAttribute("data-count");
|
|
58
|
+
if (!maxAttr && dataCount === null) return;
|
|
59
|
+
const max = maxAttr ? Number(maxAttr) : 0;
|
|
60
|
+
let counter = null;
|
|
61
|
+
const wrap = field.closest(".input-wrap, .textarea-wrap");
|
|
62
|
+
if (wrap) {
|
|
63
|
+
counter = wrap.querySelector(".input-counter");
|
|
64
|
+
if (!counter) {
|
|
65
|
+
counter = document.createElement("span");
|
|
66
|
+
counter.className = "input-counter";
|
|
67
|
+
wrap.appendChild(counter);
|
|
68
|
+
if (wrap.classList.contains("input-wrap")) {
|
|
69
|
+
wrap.classList.add("input-wrap--has-counter");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const counterEl = counter;
|
|
74
|
+
if (!counterEl) return;
|
|
75
|
+
function update() {
|
|
76
|
+
const len = field.value.length;
|
|
77
|
+
counterEl.textContent = max > 0 ? `${len} / ${max}` : `${len}`;
|
|
78
|
+
counterEl.classList.remove("is-warn", "is-error");
|
|
79
|
+
if (max > 0) {
|
|
80
|
+
const ratio = len / max;
|
|
81
|
+
if (ratio >= 1) counterEl.classList.add("is-error");
|
|
82
|
+
else if (ratio >= 0.8) counterEl.classList.add("is-warn");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
field.addEventListener("input", () => {
|
|
86
|
+
if (!isComposing) update();
|
|
87
|
+
});
|
|
88
|
+
update();
|
|
89
|
+
}
|
|
90
|
+
function setupMask(field) {
|
|
91
|
+
const mask = field.getAttribute("data-mask");
|
|
92
|
+
if (!mask) return;
|
|
93
|
+
const maskChars = [];
|
|
94
|
+
for (const ch of mask) maskChars.push(ch);
|
|
95
|
+
function isInputSlot(ch) {
|
|
96
|
+
return ch === "#" || ch === "A" || ch === "*";
|
|
97
|
+
}
|
|
98
|
+
function validate(rawChar, slotType) {
|
|
99
|
+
if (slotType === "#") return /[0-9]/.test(rawChar);
|
|
100
|
+
if (slotType === "A") return /[a-zA-Z]/.test(rawChar);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
function applyMask(input) {
|
|
104
|
+
let result = "";
|
|
105
|
+
let rawIdx = 0;
|
|
106
|
+
const rawChars = input.split("");
|
|
107
|
+
for (let i = 0; i < maskChars.length && rawIdx < rawChars.length; i++) {
|
|
108
|
+
const slot = maskChars[i];
|
|
109
|
+
if (isInputSlot(slot)) {
|
|
110
|
+
while (rawIdx < rawChars.length && !validate(rawChars[rawIdx], slot)) {
|
|
111
|
+
rawIdx++;
|
|
112
|
+
}
|
|
113
|
+
if (rawIdx < rawChars.length) {
|
|
114
|
+
result += rawChars[rawIdx];
|
|
115
|
+
rawIdx++;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
result += slot;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function extractRaw(formatted) {
|
|
124
|
+
let raw = "";
|
|
125
|
+
for (let i = 0; i < formatted.length && i < maskChars.length; i++) {
|
|
126
|
+
if (isInputSlot(maskChars[i])) raw += formatted[i];
|
|
127
|
+
}
|
|
128
|
+
return raw;
|
|
129
|
+
}
|
|
130
|
+
let lastFormatted = applyMask(field.value);
|
|
131
|
+
field.value = lastFormatted;
|
|
132
|
+
field.setAttribute("data-raw-value", extractRaw(lastFormatted));
|
|
133
|
+
field.addEventListener("input", () => {
|
|
134
|
+
if (isComposing) return;
|
|
135
|
+
const cursorPos = field.selectionStart ?? 0;
|
|
136
|
+
const oldRaw = extractRaw(lastFormatted);
|
|
137
|
+
const newFull = field.value;
|
|
138
|
+
const newRaw = extractRaw(newFull);
|
|
139
|
+
const formatted = applyMask(newRaw);
|
|
140
|
+
field.value = formatted;
|
|
141
|
+
field.setAttribute("data-raw-value", extractRaw(formatted));
|
|
142
|
+
lastFormatted = formatted;
|
|
143
|
+
const rawDelta = newRaw.length - oldRaw.length;
|
|
144
|
+
let targetCursor = cursorPos;
|
|
145
|
+
if (rawDelta > 0) {
|
|
146
|
+
let count = 0;
|
|
147
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
148
|
+
if (isInputSlot(maskChars[i])) {
|
|
149
|
+
count++;
|
|
150
|
+
if (count >= newRaw.length) {
|
|
151
|
+
targetCursor = i + 1;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else if (rawDelta < 0) {
|
|
157
|
+
targetCursor = Math.min(cursorPos, formatted.length);
|
|
158
|
+
}
|
|
159
|
+
field.setSelectionRange(targetCursor, targetCursor);
|
|
160
|
+
});
|
|
161
|
+
field.addEventListener("keydown", (ev) => {
|
|
162
|
+
if (isComposing) return;
|
|
163
|
+
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
164
|
+
if (["Backspace", "Delete", "Tab", "Escape", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(ev.key)) return;
|
|
165
|
+
const currentRaw = extractRaw(field.value);
|
|
166
|
+
const nextSlotIdx = maskChars.findIndex((ch, i) => {
|
|
167
|
+
if (!isInputSlot(ch)) return false;
|
|
168
|
+
return i >= field.value.length || !isInputSlot(maskChars[Math.min(field.value.length, maskChars.length - 1)]);
|
|
169
|
+
});
|
|
170
|
+
const nextSlot = maskChars.find((ch, i) => {
|
|
171
|
+
const filledSlots = maskChars.slice(0, i).filter(isInputSlot).length;
|
|
172
|
+
return isInputSlot(ch) && filledSlots >= currentRaw.length;
|
|
173
|
+
});
|
|
174
|
+
if (nextSlot && !validate(ev.key, nextSlot)) {
|
|
175
|
+
ev.preventDefault();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function setupTrim(field) {
|
|
180
|
+
if (!field.hasAttribute("data-trim")) return;
|
|
181
|
+
field.addEventListener("blur", () => {
|
|
182
|
+
const trimmed = field.value.trim();
|
|
183
|
+
if (trimmed !== field.value) {
|
|
184
|
+
field.value = trimmed;
|
|
185
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
46
189
|
function setupOtp(group) {
|
|
47
190
|
const el = group;
|
|
48
191
|
if (el.__icenInputInit) return;
|
|
@@ -57,6 +200,7 @@ function setupOtp(group) {
|
|
|
57
200
|
}
|
|
58
201
|
syncFilled(cell);
|
|
59
202
|
cell.addEventListener("input", () => {
|
|
203
|
+
if (isComposing) return;
|
|
60
204
|
const ch = cell.value.slice(-1);
|
|
61
205
|
cell.value = ch;
|
|
62
206
|
syncFilled(cell);
|
|
@@ -111,6 +255,31 @@ function initInput(root = document) {
|
|
|
111
255
|
if (root instanceof HTMLTextAreaElement && root.matches("[data-autosize]")) textareas.push(root);
|
|
112
256
|
textareas.push(...Array.from(root.querySelectorAll("textarea[data-autosize]")));
|
|
113
257
|
for (const ta of textareas) setupAutosize(ta);
|
|
258
|
+
const countedFields = [];
|
|
259
|
+
if (root instanceof Element) {
|
|
260
|
+
const fields = Array.from(root.querySelectorAll(
|
|
261
|
+
"input[maxlength], input[data-count], textarea[maxlength], textarea[data-count]"
|
|
262
|
+
));
|
|
263
|
+
countedFields.push(...fields);
|
|
264
|
+
if (root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) {
|
|
265
|
+
if (root.hasAttribute("maxlength") || root.hasAttribute("data-count")) countedFields.push(root);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const f of countedFields) setupCounter(f);
|
|
269
|
+
const maskedInputs = [];
|
|
270
|
+
if (root instanceof Element) {
|
|
271
|
+
maskedInputs.push(...Array.from(root.querySelectorAll("input[data-mask]")));
|
|
272
|
+
if (root instanceof HTMLInputElement && root.hasAttribute("data-mask")) maskedInputs.push(root);
|
|
273
|
+
}
|
|
274
|
+
for (const inp of maskedInputs) setupMask(inp);
|
|
275
|
+
const trimFields = [];
|
|
276
|
+
if (root instanceof Element) {
|
|
277
|
+
trimFields.push(...Array.from(root.querySelectorAll("[data-trim]")));
|
|
278
|
+
if ((root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) && root.hasAttribute("data-trim")) {
|
|
279
|
+
trimFields.push(root);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const f of trimFields) setupTrim(f);
|
|
114
283
|
const otps = [];
|
|
115
284
|
if (root instanceof Element && root.matches(".otp")) otps.push(root);
|
|
116
285
|
otps.push(...Array.from(root.querySelectorAll(".otp")));
|