@icen.ai/ui 0.3.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.
Files changed (78) hide show
  1. package/dist/behaviors/back-top.mjs +84 -0
  2. package/dist/behaviors/charts.mjs +188 -0
  3. package/dist/behaviors/command-palette.mjs +177 -0
  4. package/dist/behaviors/datatable.mjs +244 -24
  5. package/dist/behaviors/date-picker.mjs +228 -0
  6. package/dist/behaviors/input.mjs +170 -1
  7. package/dist/behaviors/notification.mjs +754 -0
  8. package/dist/behaviors/select.mjs +151 -22
  9. package/dist/behaviors/split-pane.mjs +102 -0
  10. package/dist/behaviors/upload.mjs +165 -14
  11. package/dist/components/back-top.css +88 -0
  12. package/dist/components/back-top.mjs +3 -0
  13. package/dist/components/badge.css +131 -0
  14. package/dist/components/badge.mjs +2 -0
  15. package/dist/components/breadcrumb.css +57 -0
  16. package/dist/components/card.css +68 -0
  17. package/dist/components/chart-area.css +15 -0
  18. package/dist/components/chart-area.mjs +4 -0
  19. package/dist/components/chart-bar.css +79 -0
  20. package/dist/components/chart-bar.mjs +4 -0
  21. package/dist/components/chart-gauge.css +38 -0
  22. package/dist/components/chart-gauge.mjs +4 -0
  23. package/dist/components/chart-heatmap.css +52 -0
  24. package/dist/components/chart-heatmap.mjs +4 -0
  25. package/dist/components/chart-line.css +15 -0
  26. package/dist/components/chart-line.mjs +4 -0
  27. package/dist/components/chart-pie.css +26 -0
  28. package/dist/components/chart-pie.mjs +4 -0
  29. package/dist/components/chart-radar.css +19 -0
  30. package/dist/components/chart-radar.mjs +4 -0
  31. package/dist/components/chart-sparkline.css +14 -0
  32. package/dist/components/chart-sparkline.mjs +4 -0
  33. package/dist/components/chart-stack.css +30 -0
  34. package/dist/components/chart-stack.mjs +4 -0
  35. package/dist/components/charts.css +7 -207
  36. package/dist/components/charts.mjs +9 -0
  37. package/dist/components/command-palette.css +246 -0
  38. package/dist/components/command-palette.mjs +3 -0
  39. package/dist/components/content.css +214 -0
  40. package/dist/components/copy.css +93 -0
  41. package/dist/components/copy.mjs +3 -0
  42. package/dist/components/datatable.css +86 -0
  43. package/dist/components/date-picker.css +244 -0
  44. package/dist/components/date-picker.mjs +3 -0
  45. package/dist/components/empty.css +73 -3
  46. package/dist/components/feedback.css +239 -1
  47. package/dist/components/form.css +119 -0
  48. package/dist/components/form.mjs +2 -0
  49. package/dist/components/input.css +24 -1
  50. package/dist/components/media.css +144 -1
  51. package/dist/components/nav.css +99 -0
  52. package/dist/components/notification.css +387 -0
  53. package/dist/components/notification.mjs +3 -0
  54. package/dist/components/panel.css +100 -5
  55. package/dist/components/pill.css +70 -0
  56. package/dist/components/scroll-area.css +112 -0
  57. package/dist/components/scroll-area.mjs +2 -0
  58. package/dist/components/segmented.css +75 -0
  59. package/dist/components/select.css +97 -2
  60. package/dist/components/sidebar.css +118 -0
  61. package/dist/components/slider.css +113 -0
  62. package/dist/components/split-pane.css +112 -0
  63. package/dist/components/split-pane.mjs +3 -0
  64. package/dist/components/stat.css +129 -6
  65. package/dist/components/steps.css +97 -0
  66. package/dist/components/switch.css +142 -0
  67. package/dist/components/table.css +86 -3
  68. package/dist/components/tabs.css +122 -1
  69. package/dist/components/tag.css +164 -0
  70. package/dist/components/tag.mjs +2 -0
  71. package/dist/components/toast.css +114 -0
  72. package/dist/components/toolbar.css +101 -0
  73. package/dist/components/toolbar.mjs +2 -0
  74. package/dist/components/upload.css +61 -4
  75. package/dist/index.mjs +2039 -62
  76. package/dist/registry.json +200 -24
  77. package/dist/ui.css +4865 -622
  78. package/package.json +1 -1
@@ -155,6 +155,8 @@ function svgIcon(d, size = 12) {
155
155
  var ICON_SEARCH = "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm10 2-4.3-4.3";
156
156
  var ICON_FUNNEL = "M22 3H2l8 9.46V19l4 2v-8.54L22 3Z";
157
157
  var ICON_CHEVRON = "m6 9 6 6 6-6";
158
+ var ICON_COLUMNS = "M3 3h7v18H3zM14 3h7v18h-7z";
159
+ var ICON_DOWNLOAD = "M12 3v12m0 0-4-4m4 4 4-4M5 21h14";
158
160
  function defaultCompare(a, b) {
159
161
  if (typeof a === "number" && typeof b === "number") return a - b;
160
162
  return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
@@ -169,7 +171,11 @@ function noopHandle() {
169
171
  setFilter: noop,
170
172
  setSort: noop,
171
173
  refresh: noop,
172
- destroy: noop
174
+ destroy: noop,
175
+ getData: () => [],
176
+ exportCSV: noop,
177
+ showColumn: noop,
178
+ hideColumn: noop
173
179
  };
174
180
  }
175
181
  function createTable(el, opts) {
@@ -184,13 +190,18 @@ function createTable(el, opts) {
184
190
  const filters = /* @__PURE__ */ new Map();
185
191
  const selected = /* @__PURE__ */ new Set();
186
192
  const expanded = /* @__PURE__ */ new Set();
193
+ const colWidths = /* @__PURE__ */ new Map();
194
+ const colHidden = /* @__PURE__ */ new Map();
195
+ let lastClickedIndex = -1;
187
196
  let destroyed = false;
188
197
  const virtual = opts.virtual === true;
189
- const rowH = opts.rowHeight ?? 44;
198
+ const compact = opts.density === "compact";
199
+ const rowH = opts.rowHeight ?? (compact ? 32 : 44);
190
200
  const pageSizes = opts.pageSizes ?? [10, 20, 50];
191
201
  const selectable = opts.selectable === true;
192
202
  const expandable = !virtual ? opts.expandable : void 0;
193
- const cols = () => opts.columns.filter((c) => !c.hidden);
203
+ const resizable = opts.resizable !== false;
204
+ const cols = () => opts.columns.filter((c) => !(c.hidden || colHidden.get(c.key)));
194
205
  const keyOf = (row, index) => {
195
206
  if (typeof opts.rowKey === "function") return opts.rowKey(row);
196
207
  if (typeof opts.rowKey === "string") return String(row[opts.rowKey]);
@@ -202,7 +213,12 @@ function createTable(el, opts) {
202
213
  keyCache = /* @__PURE__ */ new WeakMap();
203
214
  data.forEach((r, i) => keyCache.set(r, keyOf(r, i)));
204
215
  }
216
+ let pipelineCache = null;
217
+ function invalidate() {
218
+ pipelineCache = null;
219
+ }
205
220
  function pipeline() {
221
+ if (pipelineCache) return pipelineCache;
206
222
  let rows = data;
207
223
  if (search) {
208
224
  const q = search.toLowerCase();
@@ -223,6 +239,7 @@ function createTable(el, opts) {
223
239
  const cmp = col?.sortFn ? (a, b) => col.sortFn(a, b) : (a, b) => defaultCompare(a[sortKey], b[sortKey]);
224
240
  rows = [...rows].sort((a, b) => sortDir === "asc" ? cmp(a, b) : -cmp(a, b));
225
241
  }
242
+ pipelineCache = rows;
226
243
  return rows;
227
244
  }
228
245
  el.textContent = "";
@@ -230,21 +247,55 @@ function createTable(el, opts) {
230
247
  root.className = "datatable";
231
248
  if (opts.striped) root.classList.add("datatable--striped");
232
249
  if (virtual) root.classList.add("datatable--virtual");
250
+ if (compact) root.classList.add("datatable--compact");
251
+ if (opts.columns.some((c) => c.frozen)) root.classList.add("datatable--frozen");
233
252
  let searchInput = null;
234
- if (opts.searchable) {
235
- const bar = document.createElement("div");
236
- bar.className = "dt-toolbar";
237
- const box = document.createElement("div");
238
- box.className = "dt-search";
239
- box.appendChild(svgIcon(ICON_SEARCH, 14));
240
- searchInput = document.createElement("input");
241
- searchInput.className = "dt-search-input";
242
- searchInput.type = "search";
243
- searchInput.placeholder = opts.searchPlaceholder ?? "\u641C\u7D22\u2026";
244
- searchInput.setAttribute("aria-label", "\u641C\u7D22\u8868\u683C");
245
- box.appendChild(searchInput);
246
- bar.appendChild(box);
247
- root.appendChild(bar);
253
+ const needsToolbar = opts.searchable || opts.columnVisibility || opts.exportable;
254
+ let toolbar = null;
255
+ if (needsToolbar) {
256
+ toolbar = document.createElement("div");
257
+ toolbar.className = "dt-toolbar";
258
+ if (opts.searchable) {
259
+ const box = document.createElement("div");
260
+ box.className = "dt-search";
261
+ box.appendChild(svgIcon(ICON_SEARCH, 14));
262
+ searchInput = document.createElement("input");
263
+ searchInput.className = "dt-search-input";
264
+ searchInput.type = "search";
265
+ searchInput.placeholder = opts.searchPlaceholder ?? "\u641C\u7D22\u2026";
266
+ searchInput.setAttribute("aria-label", "\u641C\u7D22\u8868\u683C");
267
+ box.appendChild(searchInput);
268
+ toolbar.appendChild(box);
269
+ }
270
+ const actions = document.createElement("div");
271
+ actions.className = "dt-toolbar-actions";
272
+ if (opts.columnVisibility) {
273
+ const colBtn = document.createElement("button");
274
+ colBtn.type = "button";
275
+ colBtn.className = "dt-tool-btn";
276
+ colBtn.setAttribute("aria-label", "\u5217\u8BBE\u7F6E");
277
+ colBtn.appendChild(svgIcon(ICON_COLUMNS, 14));
278
+ colBtn.addEventListener("click", (ev) => {
279
+ ev.stopPropagation();
280
+ if (openPanel?.key === "__cols") closeFilterPanel();
281
+ else {
282
+ closeFilterPanel();
283
+ openColumnPanel(colBtn);
284
+ }
285
+ });
286
+ actions.appendChild(colBtn);
287
+ }
288
+ if (opts.exportable) {
289
+ const expBtn = document.createElement("button");
290
+ expBtn.type = "button";
291
+ expBtn.className = "dt-tool-btn";
292
+ expBtn.setAttribute("aria-label", "\u5BFC\u51FA CSV");
293
+ expBtn.appendChild(svgIcon(ICON_DOWNLOAD, 14));
294
+ expBtn.addEventListener("click", () => exportCSV());
295
+ actions.appendChild(expBtn);
296
+ }
297
+ if (actions.children.length > 0) toolbar.appendChild(actions);
298
+ root.appendChild(toolbar);
248
299
  }
249
300
  const scroll = document.createElement("div");
250
301
  scroll.className = "dt-scroll";
@@ -269,11 +320,29 @@ function createTable(el, opts) {
269
320
  foot.className = "dt-foot";
270
321
  root.appendChild(foot);
271
322
  el.appendChild(root);
323
+ function frozenOffsets() {
324
+ const offsets = /* @__PURE__ */ new Map();
325
+ if (!opts.columns.some((c) => c.frozen)) return offsets;
326
+ const visible = cols();
327
+ let acc = 0;
328
+ if (selectable) acc += 40;
329
+ if (expandable) acc += 34;
330
+ for (const c of visible) {
331
+ if (c.frozen) offsets.set(c.key, acc);
332
+ const w = colWidths.get(c.key) ?? c.width ?? "minmax(0,1fr)";
333
+ acc += parsePx(w);
334
+ }
335
+ return offsets;
336
+ }
337
+ function parsePx(w) {
338
+ const m = /^(\d+(?:\.\d+)?)px/.exec(w);
339
+ return m ? Number(m[1]) : 0;
340
+ }
272
341
  function template() {
273
342
  const parts = [];
274
343
  if (selectable) parts.push("40px");
275
344
  if (expandable) parts.push("34px");
276
- for (const c of cols()) parts.push(c.width ?? "minmax(0,1fr)");
345
+ for (const c of cols()) parts.push(colWidths.get(c.key) ?? c.width ?? "minmax(0,1fr)");
277
346
  return parts.join(" ");
278
347
  }
279
348
  function fillCell(cell, col, row, rowIndex) {
@@ -355,6 +424,7 @@ function createTable(el, opts) {
355
424
  const cell = document.createElement("div");
356
425
  cell.className = "dt-cell dt-cell--head";
357
426
  if (col.align) cell.dataset.align = col.align;
427
+ if (col.frozen) cell.classList.add("dt-cell--frozen");
358
428
  const title = document.createElement("span");
359
429
  title.className = "dt-head-title";
360
430
  title.textContent = col.title;
@@ -376,10 +446,12 @@ function createTable(el, opts) {
376
446
  sortDir = "asc";
377
447
  } else if (sortDir === "asc") sortDir = "desc";
378
448
  else sortKey = null;
449
+ invalidate();
450
+ opts.onSortChange?.(sortKey, sortDir);
379
451
  renderAll();
380
452
  };
381
453
  cell.addEventListener("click", (ev) => {
382
- if (ev.target.closest(".dt-filter-btn")) return;
454
+ if (ev.target.closest(".dt-filter-btn, .dt-resize")) return;
383
455
  toggleSort();
384
456
  });
385
457
  cell.addEventListener("keydown", (ev) => {
@@ -407,9 +479,102 @@ function createTable(el, opts) {
407
479
  });
408
480
  cell.appendChild(btn);
409
481
  }
482
+ if (resizable && col !== cols()[cols().length - 1]) {
483
+ const handle = document.createElement("div");
484
+ handle.className = "dt-resize";
485
+ handle.setAttribute("role", "separator");
486
+ handle.setAttribute("aria-orientation", "vertical");
487
+ handle.setAttribute("aria-label", `\u8C03\u6574\u300C${col.title}\u300D\u5217\u5BBD`);
488
+ attachResize(handle, col);
489
+ cell.appendChild(handle);
490
+ }
410
491
  headRow.appendChild(cell);
411
492
  }
412
493
  }
494
+ function attachResize(handle, col) {
495
+ handle.addEventListener("pointerdown", (ev) => {
496
+ ev.preventDefault();
497
+ ev.stopPropagation();
498
+ const startX = ev.clientX;
499
+ const startW = handle.closest(".dt-cell").getBoundingClientRect().width;
500
+ handle.setPointerCapture(ev.pointerId);
501
+ const onMove = (e) => {
502
+ const delta = e.clientX - startX;
503
+ const next = Math.max(48, Math.round(startW + delta));
504
+ colWidths.set(col.key, `${next}px`);
505
+ headRow.style.gridTemplateColumns = template();
506
+ body.querySelectorAll(".dt-row").forEach((r) => {
507
+ r.style.gridTemplateColumns = template();
508
+ });
509
+ };
510
+ const onUp = (e) => {
511
+ handle.releasePointerCapture(e.pointerId);
512
+ handle.removeEventListener("pointermove", onMove);
513
+ handle.removeEventListener("pointerup", onUp);
514
+ };
515
+ handle.addEventListener("pointermove", onMove);
516
+ handle.addEventListener("pointerup", onUp);
517
+ });
518
+ }
519
+ function openColumnPanel(btn) {
520
+ const panel2 = document.createElement("div");
521
+ panel2.className = "dt-filter-panel dt-col-panel";
522
+ panel2.hidden = true;
523
+ panel2.addEventListener("click", (ev) => ev.stopPropagation());
524
+ const list = document.createElement("div");
525
+ list.className = "dt-filter-list";
526
+ for (const col of opts.columns) {
527
+ if (col.key === "__select" || col.key === "__expand") continue;
528
+ const row = document.createElement("label");
529
+ row.className = "dt-filter-item";
530
+ const isHidden = colHidden.get(col.key) ?? col.hidden ?? false;
531
+ const box = makeCheck(!isHidden, col.title);
532
+ box.addEventListener("change", () => {
533
+ if (box.checked) colHidden.delete(col.key);
534
+ else colHidden.set(col.key, true);
535
+ invalidate();
536
+ renderAll();
537
+ });
538
+ const text = document.createElement("span");
539
+ text.textContent = col.title;
540
+ row.append(box, text);
541
+ list.appendChild(row);
542
+ }
543
+ panel2.appendChild(list);
544
+ document.body.appendChild(panel2);
545
+ openPanel = { key: "__cols", el: panel2 };
546
+ const rect = btn.getBoundingClientRect();
547
+ const pr = panel2.getBoundingClientRect();
548
+ let x = rect.right - pr.width;
549
+ let y = rect.bottom + 4;
550
+ if (x < 8) x = 8;
551
+ if (y + pr.height > window.innerHeight - 8) y = rect.top - pr.height - 4;
552
+ panel2.style.left = `${x}px`;
553
+ panel2.style.top = `${y}px`;
554
+ panel2.hidden = false;
555
+ window.addEventListener("scroll", onPanelScroll, true);
556
+ window.addEventListener("resize", onPanelScroll);
557
+ }
558
+ function exportCSV(filename) {
559
+ const rows = pipeline();
560
+ const visible = cols();
561
+ const header = visible.map((c) => csvEscape(c.title)).join(",");
562
+ const lines = rows.map(
563
+ (row) => visible.map((c) => csvEscape(String(row[c.key] ?? ""))).join(",")
564
+ );
565
+ const csv = "\uFEFF" + [header, ...lines].join("\r\n");
566
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
567
+ const url = URL.createObjectURL(blob);
568
+ const a = document.createElement("a");
569
+ a.href = url;
570
+ a.download = filename ?? "export.csv";
571
+ a.click();
572
+ URL.revokeObjectURL(url);
573
+ }
574
+ function csvEscape(s) {
575
+ if (/[",\r\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
576
+ return s;
577
+ }
413
578
  function buildFilterPanel(col) {
414
579
  const panel2 = document.createElement("div");
415
580
  panel2.className = "dt-filter-panel";
@@ -428,6 +593,7 @@ function createTable(el, opts) {
428
593
  else cur.delete(v);
429
594
  if (cur.size === values.length) filters.delete(col.key);
430
595
  else filters.set(col.key, cur);
596
+ invalidate();
431
597
  page = 1;
432
598
  renderAll();
433
599
  });
@@ -444,6 +610,7 @@ function createTable(el, opts) {
444
610
  reset.textContent = "\u91CD\u7F6E";
445
611
  reset.addEventListener("click", () => {
446
612
  filters.delete(col.key);
613
+ invalidate();
447
614
  page = 1;
448
615
  closeFilterPanel();
449
616
  renderAll();
@@ -459,15 +626,40 @@ function createTable(el, opts) {
459
626
  tr.style.gridTemplateColumns = template();
460
627
  if (virtual) tr.style.height = `${rowH}px`;
461
628
  if (selected.has(key)) tr.classList.add("is-selected");
629
+ if (opts.rowClassName) {
630
+ const extra = opts.rowClassName(row, rowIndex);
631
+ if (extra) tr.classList.add(...extra.split(/\s+/).filter(Boolean));
632
+ }
462
633
  if (selectable) {
463
634
  const cell = document.createElement("div");
464
635
  cell.className = "dt-cell dt-cell--check";
636
+ if (frozenOffsets().size > 0 && selectable) {
637
+ cell.classList.add("dt-cell--frozen");
638
+ cell.style.left = "0px";
639
+ }
465
640
  const box = makeCheck(selected.has(key), "\u9009\u62E9\u8BE5\u884C");
466
- box.addEventListener("click", (ev) => ev.stopPropagation());
641
+ box.addEventListener("click", (ev) => {
642
+ box._shift = ev.shiftKey;
643
+ ev.stopPropagation();
644
+ });
467
645
  box.addEventListener("change", () => {
468
- if (box.checked) selected.add(key);
469
- else selected.delete(key);
470
- tr.classList.toggle("is-selected", box.checked);
646
+ const shift = box._shift === true;
647
+ if (shift && lastClickedIndex >= 0 && lastClickedIndex !== rowIndex) {
648
+ const rows = pipeline();
649
+ const [lo, hi] = lastClickedIndex < rowIndex ? [lastClickedIndex, rowIndex] : [rowIndex, lastClickedIndex];
650
+ const willSelect = box.checked;
651
+ for (let i = lo; i <= hi; i++) {
652
+ const k = keyOfRow(rows[i]);
653
+ if (willSelect) selected.add(k);
654
+ else selected.delete(k);
655
+ }
656
+ renderBody();
657
+ } else {
658
+ if (box.checked) selected.add(key);
659
+ else selected.delete(key);
660
+ tr.classList.toggle("is-selected", box.checked);
661
+ }
662
+ lastClickedIndex = rowIndex;
471
663
  emitSelection();
472
664
  renderHead();
473
665
  renderFoot(pipeline().length);
@@ -493,10 +685,16 @@ function createTable(el, opts) {
493
685
  cell.appendChild(btn);
494
686
  tr.appendChild(cell);
495
687
  }
688
+ const offsets = frozenOffsets();
496
689
  for (const col of cols()) {
497
690
  const cell = document.createElement("div");
498
691
  cell.className = "dt-cell";
499
692
  if (col.align) cell.dataset.align = col.align;
693
+ if (col.frozen) {
694
+ cell.classList.add("dt-cell--frozen");
695
+ const off = offsets.get(col.key);
696
+ if (off != null) cell.style.left = `${off}px`;
697
+ }
500
698
  fillCell(cell, col, row, rowIndex);
501
699
  tr.appendChild(cell);
502
700
  }
@@ -663,6 +861,7 @@ function createTable(el, opts) {
663
861
  window.clearTimeout(searchTimer);
664
862
  searchTimer = window.setTimeout(() => {
665
863
  search = searchInput?.value.trim() ?? "";
864
+ invalidate();
666
865
  page = 1;
667
866
  renderAll();
668
867
  }, 160);
@@ -676,7 +875,7 @@ function createTable(el, opts) {
676
875
  function onDocPointerDown(ev) {
677
876
  if (!openPanel) return;
678
877
  const t = ev.target;
679
- if (t instanceof Node && (openPanel.el.contains(t) || headRow.contains(t))) return;
878
+ if (t instanceof Node && (openPanel.el.contains(t) || headRow.contains(t) || toolbar?.contains(t))) return;
680
879
  closeFilterPanel();
681
880
  }
682
881
  document.addEventListener("pointerdown", onDocPointerDown, true);
@@ -700,6 +899,7 @@ function createTable(el, opts) {
700
899
  setData(next) {
701
900
  data = next;
702
901
  rekey();
902
+ invalidate();
703
903
  page = 1;
704
904
  renderAll();
705
905
  },
@@ -714,21 +914,41 @@ function createTable(el, opts) {
714
914
  setSearch(q) {
715
915
  search = q;
716
916
  if (searchInput) searchInput.value = q;
917
+ invalidate();
717
918
  page = 1;
718
919
  renderAll();
719
920
  },
720
921
  setFilter(key, values) {
721
922
  if (values === null) filters.delete(key);
722
923
  else filters.set(key, new Set(values));
924
+ invalidate();
723
925
  page = 1;
724
926
  renderAll();
725
927
  },
726
928
  setSort(key, dir = "asc") {
727
929
  sortKey = key;
728
930
  sortDir = dir;
931
+ invalidate();
932
+ opts.onSortChange?.(sortKey, sortDir);
933
+ renderAll();
934
+ },
935
+ getData() {
936
+ return pipeline();
937
+ },
938
+ exportCSV,
939
+ showColumn(key) {
940
+ colHidden.delete(key);
941
+ opts.columns.find((c) => c.key === key).hidden = false;
942
+ invalidate();
943
+ renderAll();
944
+ },
945
+ hideColumn(key) {
946
+ colHidden.set(key, true);
947
+ invalidate();
729
948
  renderAll();
730
949
  },
731
950
  refresh() {
951
+ invalidate();
732
952
  renderAll();
733
953
  },
734
954
  destroy() {
@@ -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
+ };