diff --git a/dist/datatable.js b/dist/datatable.js
index b8c014e..e0bd832 100644
--- a/dist/datatable.js
+++ b/dist/datatable.js
@@ -1,722 +1,965 @@
-import { Component, NewState, RefreshState, Util } from "@web/state";
-import { VirtualScroll, State } from "@web/base";
-const createPerfMonitor = () => {
- let enabled = !!window.__DT_PERF_MODE__;
- const stats = { refreshTime: 0, refreshCount: 0, scrollCount: 0, totalNodes: 0 };
- if (enabled && !window.__statePerformanceTelemetry) {
- window.__statePerformanceTelemetry = { scanCount: 0, reuseCount: 0, moveCount: 0 };
- }
- return {
- get stats() {
- return stats;
+(function(factory) {
+ typeof define === "function" && define.amd ? define(factory) : factory();
+})(function() {
+ "use strict";
+ const MODE_MAP = {
+ text: ["contains", "equals", "starts", "ends"],
+ textarea: ["contains", "equals", "starts", "ends"],
+ number: ["=", ">", "<", "between"],
+ date: ["=", ">", "<", "between"],
+ select: ["contains", "equals"],
+ TagsInput: ["contains", "equals", "starts", "ends"]
+ };
+ const MODE_ICONS = {
+ "contains": "bi-search",
+ "equals": "bi-distribute-vertical",
+ "starts": "bi-align-start",
+ "ends": "bi-align-end",
+ "=": "bi-calculator",
+ ">": "bi-chevron-right",
+ "<": "bi-chevron-left",
+ "between": "bi-arrows-expand"
+ };
+ const DataTableConfig = {
+ _fieldTypes: /* @__PURE__ */ new Map(),
+ registerFieldType: (config) => {
+ DataTableConfig._fieldTypes.set(config.value, config);
},
- enable: () => {
- enabled = true;
- },
- disable: () => {
- enabled = false;
- },
- onScroll: () => {
- if (enabled) stats.scrollCount++;
- },
- startFrame: () => {
- var _a, _b, _c;
- if (!enabled) return null;
- return {
- start: performance.now(),
- scan: ((_a = window.__statePerformanceTelemetry) == null ? void 0 : _a.scanCount) || 0,
- move: ((_b = window.__statePerformanceTelemetry) == null ? void 0 : _b.moveCount) || 0,
- reuse: ((_c = window.__statePerformanceTelemetry) == null ? void 0 : _c.reuseCount) || 0
- };
- },
- endFrame: (startData, renderedCount) => {
- if (!enabled || !startData) return;
- stats.refreshCount++;
- stats.totalNodes += renderedCount;
- const elapsed = performance.now() - startData.start;
- stats.refreshTime += elapsed;
- const stPerf = window.__statePerformanceTelemetry;
- if (stPerf) {
- const scans = stPerf.scanCount - startData.scan;
- const moves = stPerf.moveCount - startData.move;
- const reuses = stPerf.reuseCount - startData.reuse;
- if (scans > 0 || elapsed > 2) {
- console.log(`[DataTable Frame] Time: ${elapsed.toFixed(2)}ms, Scans: ${scans}, Moves: ${moves}, Reuses: ${reuses}, Rows: ${renderedCount}`);
- }
+ getFieldTypes: () => Array.from(DataTableConfig._fieldTypes.values())
+ };
+ DataTableConfig.registerFieldType({
+ value: "text",
+ label: "{#Text#}",
+ typeForDB: "v4096",
+ schema: [{ name: "placeholder", label: "Placeholder", type: "text", if: 'this.data.user_type=="text"' }]
+ });
+ DataTableConfig.registerFieldType({
+ value: "number",
+ label: "{#Number#}",
+ typeForDB: "ff",
+ schema: [
+ { name: "decimals", label: "Decimals", type: "number", setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' },
+ { name: "prefix", label: "Prefix (e.g. $)", type: "text", if: 'this.data.user_type=="number"' },
+ { name: "suffix", label: "Suffix (e.g. %)", type: "text", if: 'this.data.user_type=="number"' },
+ { name: "thousandSep", label: "Thousand Sep", type: "switch", if: 'this.data.user_type=="number"' }
+ ],
+ formatter: (val, field) => {
+ if (val == null || val === "") return "";
+ let num = Number(val);
+ if (isNaN(num)) return val;
+ const s = field.settings || {};
+ if (s.decimals !== void 0) num = num.toFixed(s.decimals);
+ let str = String(num);
+ if (s.thousandSep) {
+ const parts = str.split(".");
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ str = parts.join(".");
}
+ return (s.prefix || "") + str + (s.suffix || "");
}
- };
-};
-const createScrollManager = (container, state, onRenderedListChange) => {
- const vs = VirtualScroll({ itemHeight: 40 });
- let scrollEl = null;
- const refresh = (isLayoutChange = false) => {
- if (!scrollEl) return;
- const res = vs.calc(scrollEl, state.list);
- if (res) {
- if (!isLayoutChange && state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) {
- return;
- }
- Object.assign(state, {
- prevHeight: res.prevHeight,
- postHeight: res.postHeight,
- _listStartIndex: res.listStartIndex,
- _renderedList: res.renderedList
- });
- onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length, isLayoutChange);
- }
- };
- return {
- init: () => {
- scrollEl = container.querySelector(".dt-main");
- },
- reset: (list) => {
- state._listStartIndex = 0;
- vs.reset(list, scrollEl || container);
- if (state.list === list) {
- vs.init(list, () => refresh(true));
- }
- },
- refresh,
- onScroll: () => refresh(false)
- };
-};
-const createSelectionManager = (container, state) => {
- let activeBounds = null;
- let startCell = null;
- let multiSelections = [];
- const isCellSelected = (r, c) => {
- if (activeBounds && r >= activeBounds.minRow && r <= activeBounds.maxRow && c >= activeBounds.minCol && c <= activeBounds.maxCol) return true;
- return multiSelections.some((s) => r >= s.minRow && r <= s.maxRow && c >= s.minCol && c <= s.maxCol);
- };
- let lastHadSelection = false;
- const applySelectionUI = () => {
- if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return;
- let boundMinRow = Infinity, boundMaxRow = -Infinity;
- if (activeBounds) {
- boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
- boundMaxRow = Math.max(boundMaxRow, activeBounds.maxRow);
- }
- multiSelections.forEach((s) => {
- boundMinRow = Math.min(boundMinRow, s.minRow);
- boundMaxRow = Math.max(boundMaxRow, s.maxRow);
- });
- const hasSelection = boundMinRow !== Infinity;
- if (!hasSelection && !lastHadSelection) return;
- lastHadSelection = hasSelection;
- const body = container.querySelector(".dt-body");
- if (!body) return;
- const rowNodes = body.querySelectorAll(".dt-body-row");
- rowNodes.forEach((rowNode) => {
+ });
+ DataTableConfig.registerFieldType({
+ value: "select",
+ label: "{#Single Select#}",
+ typeForDB: "v1024",
+ schema: [{ name: "options_str", label: "Options", type: "textarea", setting: { rows: 3, placeholder: "Label:Value per line" }, if: 'this.data.user_type=="select"' }],
+ formatter: (val, field) => {
var _a;
- const absoluteRow = (((_a = rowNode._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex;
- const cells = rowNode.querySelectorAll(".dt-cell");
- if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
- cells.forEach((cell) => cell.classList.remove("dt-cell-selected"));
- return;
- }
- cells.forEach((cell, cIdx) => {
- if (isCellSelected(absoluteRow, cIdx)) {
- cell.classList.add("dt-cell-selected");
- } else {
- cell.classList.remove("dt-cell-selected");
- }
- });
- });
- };
- const updateStatus = () => {
- let count = 0;
- if (activeBounds) count += activeBounds.maxRow - activeBounds.minRow + 1;
- multiSelections.forEach((s) => count += s.maxRow - s.minRow + 1);
- state.selectedRowCount = count;
- };
- const clearAllActive = (keepSelection = false) => {
- if (!keepSelection) {
- activeBounds = null;
- startCell = null;
- multiSelections = [];
- applySelectionUI();
- updateStatus();
+ if (val == null || val === "") return "";
+ const opts = ((_a = field.settings) == null ? void 0 : _a.options) || [];
+ const opt = opts.find((o) => typeof o === "object" ? o.value == val : o == val);
+ return opt ? typeof opt === "object" ? opt.label : opt : val;
}
- };
- const startSelect = (row, col, e) => {
- const alreadySelected = isCellSelected(row, col);
- const isRange = activeBounds && (activeBounds.minRow !== activeBounds.maxRow || activeBounds.minCol !== activeBounds.maxCol) || multiSelections.length > 0;
- if (e.shiftKey && startCell) {
- activeBounds = {
- minRow: Math.min(startCell.row, row),
- maxRow: Math.max(startCell.row, row),
- minCol: Math.min(startCell.col, col),
- maxCol: Math.max(startCell.col, col)
- };
- } else {
- if (alreadySelected && !e.ctrlKey && !e.metaKey) {
- if (!isRange) container._potentialCancel = { row, col };
- } else {
- if (!e.ctrlKey && !e.metaKey) {
- clearAllActive();
- } else if (activeBounds && !alreadySelected) {
- multiSelections.push(activeBounds);
- }
- startCell = { row, col };
- activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col };
- }
- state.isSelecting = true;
+ });
+ DataTableConfig.registerFieldType({
+ value: "checkbox",
+ label: "{#Multi Select#}",
+ typeForDB: "v4096",
+ schema: [{ name: "options_str", label: "Options", type: "textarea", setting: { rows: 3, placeholder: "Label:Value per line" }, if: 'this.data.user_type=="checkbox"' }],
+ formatter: (val, field) => {
+ var _a;
+ if (!Array.isArray(val)) return val == null ? "" : String(val);
+ const opts = ((_a = field.settings) == null ? void 0 : _a.options) || [];
+ return val.map((v) => {
+ const opt = opts.find((o) => typeof o === "object" ? o.value == v : o == v);
+ return opt ? typeof opt === "object" ? opt.label : opt : v;
+ }).join(", ");
}
- applySelectionUI();
- updateStatus();
- container.focus();
- };
- const updateSelect = (row, col) => {
- if (state.isSelecting && startCell) {
- activeBounds = {
- minRow: Math.min(startCell.row, row),
- maxRow: Math.max(startCell.row, row),
- minCol: Math.min(startCell.col, col),
- maxCol: Math.max(startCell.col, col)
- };
- container._potentialCancel = null;
- applySelectionUI();
- updateStatus();
+ });
+ DataTableConfig.registerFieldType({
+ value: "switch",
+ label: "{#Switch#}",
+ typeForDB: "b",
+ schema: [
+ { name: "labelOn", label: "Label On", type: "text", if: 'this.data.user_type=="switch"' },
+ { name: "labelOff", label: "Label Off", type: "text", if: 'this.data.user_type=="switch"' }
+ ],
+ formatter: (val, field) => {
+ const s = field.settings || {};
+ return val ? s.labelOn || "Yes" : s.labelOff || "No";
}
- };
- const endSelect = () => {
- if (container._potentialCancel) {
- const { row, col } = container._potentialCancel;
- if (isCellSelected(row, col)) {
- clearAllActive();
- }
- container._potentialCancel = null;
+ });
+ DataTableConfig.registerFieldType({
+ value: "datetime",
+ label: "{#DateTime#}",
+ typeForDB: "dt",
+ schema: [{ name: "format", label: "Format", type: "text", setting: { placeholder: "YYYY-MM-DD" }, if: 'this.data.user_type=="datetime"' }]
+ });
+ DataTableConfig.registerFieldType({
+ value: "textarea",
+ label: "{#Long Text#}",
+ typeForDB: "t",
+ schema: [{ name: "placeholder", label: "Placeholder", type: "text", if: 'this.data.user_type=="textarea"' }]
+ });
+ const createPerfMonitor = () => {
+ let enabled = !!globalThis.__DT_PERF_MODE__;
+ const stats = { refreshTime: 0, refreshCount: 0, scrollCount: 0, totalNodes: 0 };
+ if (enabled && !globalThis.__statePerformanceTelemetry) {
+ globalThis.__statePerformanceTelemetry = { scanCount: 0, reuseCount: 0, moveCount: 0 };
}
- state.isSelecting = false;
- };
- const getSelectionBounds = () => {
- if (!activeBounds) return null;
- let minRow = activeBounds.minRow, maxRow = activeBounds.maxRow;
- let minCol = activeBounds.minCol, maxCol = activeBounds.maxCol;
- multiSelections.forEach((s) => {
- minRow = Math.min(minRow, s.minRow);
- maxRow = Math.max(maxRow, s.maxRow);
- minCol = Math.min(minCol, s.minCol);
- maxCol = Math.max(maxCol, s.maxCol);
- });
- return { minRow, maxRow, minCol, maxCol };
- };
- const copy = async () => {
- const bounds = getSelectionBounds();
- if (!bounds) return;
- const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map((row) => {
- return state.fields.slice(bounds.minCol, bounds.maxCol + 1).map((f) => {
- let val = String(row[f.id] ?? "");
- if (val.includes(" ") || val.includes("\n") || val.includes('"')) {
- val = '"' + val.replace(/"/g, '""') + '"';
- }
- return val;
- }).join(" ");
- }).join("\n");
- await navigator.clipboard.writeText(text);
- };
- const paste = async () => {
- try {
- const text = await navigator.clipboard.readText();
- if (!text) return;
- const bounds = getSelectionBounds();
- if (!bounds) return;
- const rows = text.split(/\r?\n/).filter((line) => line.length > 0).map((line) => {
- const cells = [];
- let current = "", inQuotes = false;
- for (let i = 0; i < line.length; i++) {
- const char = line[i];
- if (char === '"') {
- if (inQuotes && line[i + 1] === '"') {
- current += '"';
- i++;
- } else inQuotes = !inQuotes;
- } else if (char === " " && !inQuotes) {
- cells.push(current);
- current = "";
- } else {
- current += char;
+ return {
+ get stats() {
+ return stats;
+ },
+ enable: () => {
+ enabled = true;
+ },
+ disable: () => {
+ enabled = false;
+ },
+ onScroll: () => {
+ if (enabled) stats.scrollCount++;
+ },
+ startFrame: () => {
+ var _a, _b, _c;
+ if (!enabled) return null;
+ return {
+ start: performance.now(),
+ scan: ((_a = globalThis.__statePerformanceTelemetry) == null ? void 0 : _a.scanCount) || 0,
+ move: ((_b = globalThis.__statePerformanceTelemetry) == null ? void 0 : _b.moveCount) || 0,
+ reuse: ((_c = globalThis.__statePerformanceTelemetry) == null ? void 0 : _c.reuseCount) || 0
+ };
+ },
+ endFrame: (startData, renderedCount) => {
+ if (!enabled || !startData) return;
+ stats.refreshCount++;
+ stats.totalNodes += renderedCount;
+ const elapsed = performance.now() - startData.start;
+ stats.refreshTime += elapsed;
+ const stPerf = globalThis.__statePerformanceTelemetry;
+ if (stPerf) {
+ const scans = stPerf.scanCount - startData.scan;
+ const moves = stPerf.moveCount - startData.move;
+ const reuses = stPerf.reuseCount - startData.reuse;
+ if (scans > 0 || elapsed > 2) {
+ console.log(`[DataTable Frame] Time: ${elapsed.toFixed(2)}ms, Scans: ${scans}, Moves: ${moves}, Reuses: ${reuses}, Rows: ${renderedCount}`);
}
}
- cells.push(current);
- return cells;
- });
- const startRow = bounds.minRow;
- const startCol = bounds.minCol;
- const maxRow = bounds.maxRow;
- const maxCol = bounds.maxCol;
- const body = container.querySelector(".dt-body");
- const rowNodes = body ? Array.from(body.childNodes).filter((n) => {
- var _a;
- return (_a = n.classList) == null ? void 0 : _a.contains("dt-body-row");
- }) : [];
- rows.forEach((rowData, rOffset) => {
- const rIdx = startRow + rOffset;
- if (rIdx > maxRow || rIdx >= state.list.length) return;
- const rowItem = state.list[rIdx];
- let rowChanged = false;
- rowData.forEach((cellData, cOffset) => {
- const cIdx = startCol + cOffset;
- if (cIdx > maxCol || cIdx >= state.fields.length) return;
- const field = state.fields[cIdx];
- rowItem[field.id] = cellData;
- rowChanged = true;
- });
- if (rowChanged && container.refreshNode) {
- const domNode = rowNodes.find((n) => {
- var _a;
- return (((_a = n._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex === rIdx;
- });
- if (domNode) container.refreshNode(domNode);
- }
- });
- } catch (err) {
- console.error("Paste Error:", err);
- }
- };
- const deleteSelected = () => {
- const bounds = getSelectionBounds();
- if (!bounds) return;
- const rMin = bounds.minRow, rMax = bounds.maxRow;
- const newList = state.list.filter((_, i) => !(i >= rMin && i <= rMax));
- state.list = newList;
- clearAllActive();
- };
- return {
- applySelectionUI,
- clearAllActive,
- startSelect,
- updateSelect,
- endSelect,
- deleteSelected,
- getSelectionBounds,
- copy,
- paste
- };
-};
-const MODE_MAP = {
- text: ["contains", "equals", "starts", "ends"],
- textarea: ["contains", "equals", "starts", "ends"],
- number: ["=", ">", "<", "between"],
- date: ["=", ">", "<", "between"],
- select: ["contains", "equals"],
- TagsInput: ["contains", "equals", "starts", "ends"]
-};
-const MODE_ICONS = {
- "contains": "bi-search",
- "equals": "bi-distribute-vertical",
- "starts": "bi-align-start",
- "ends": "bi-align-end",
- "=": "bi-calculator",
- ">": "bi-chevron-right",
- "<": "bi-chevron-left",
- "between": "bi-arrows-expand"
-};
-Component.register("DataTable", (container) => {
- if (!container.state) container.state = NewState({});
- const state = container.state;
- Object.assign(state, {
- list: [],
- fields: [],
- _renderedList: [],
- prevHeight: 0,
- postHeight: 0,
- _listStartIndex: 0,
- selectedRowCount: 0,
- _originalList: [],
- sortConfig: { fieldId: null, direction: null },
- filterConfig: {},
- // fieldId -> { mode, value, value2, selectedValues: [] }
- activeFieldId: null,
- activeField: null,
- activeModes: [],
- _columnStats: {},
- _internalUpdate: false,
- _appliedHash: "",
- _fieldsDirty: false,
- _masterCellNodes: null
- });
- const perf = createPerfMonitor();
- state.perf = perf.stats;
- const selection = createSelectionManager(container, state);
- const scroll = createScrollManager(container, state, () => selection.applySelectionUI());
- const menuNode = container.querySelector(".dt-column-menu");
- if (menuNode) menuNode._thisObj = container;
- container.onColumnResizing = (field, e) => container.style.setProperty(`--w-${field.id}`, e.detail.newSize + "px");
- container.onColumnResize = (field, e) => {
- const idx = state.fields.findIndex((f) => f.id === field.id);
- if (idx !== -1) {
- state.fields[idx].width = e.detail.newSize;
- state.fields = [...state.fields];
- }
- };
- let _editorOverlay, currentEditingNode = null;
- container.format = (val, field) => {
- if (field.formatter) return field.formatter(val, field);
- return val == null ? "" : typeof val === "object" ? JSON.stringify(val) : String(val);
- };
- container.onScroll = () => {
- perf.onScroll();
- scroll.refresh();
- container.hideColumnMenu();
- const prev = container.querySelector(".dt-spacer-prev"), post = container.querySelector(".dt-spacer-post");
- if (prev) {
- prev.style.height = (state.prevHeight || 0) + "px";
- prev.style.display = state.prevHeight > 0 ? "block" : "none";
- }
- if (post) {
- post.style.height = (state.postHeight || 0) + "px";
- post.style.display = state.postHeight > 0 ? "block" : "none";
- }
- };
- container.applySortFilter = (options = {}) => {
- if (state._internalUpdate) return;
- const targetFilters = { ...state.filterConfig, ...options.filters || {} };
- const targetSort = options.sort !== void 0 ? options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null } : state.sortConfig;
- const currentHash = JSON.stringify({ s: targetSort, f: targetFilters });
- if (state._appliedHash === currentHash && !options.force) return;
- state._internalUpdate = true;
- let list = [...state._originalList || []];
- Object.keys(targetFilters).forEach((fieldId) => {
- const filter = targetFilters[fieldId];
- if (!filter) return;
- const { mode = "contains", value, value2, selectedValues } = filter;
- if ((selectedValues == null ? void 0 : selectedValues.length) > 0) {
- list = list.filter((item) => selectedValues.includes(String(item[fieldId] ?? "")));
- return;
}
- if (value === "" || value == null) return;
- const lowV = String(value).toLowerCase(), n1 = Number(value), n2 = Number(value2);
- list = list.filter((item) => {
- const iv = item[fieldId], sv = String(iv ?? "").toLowerCase();
- switch (mode) {
- case "contains":
- return sv.includes(lowV);
- case "equals":
- return sv === lowV;
- case "starts":
- return sv.startsWith(lowV);
- case "ends":
- return sv.endsWith(lowV);
- case ">":
- return Number(iv) > n1;
- case "<":
- return Number(iv) < n1;
- case "=":
- return Number(iv) === n1;
- case "between":
- return Number(iv) >= n1 && Number(iv) <= n2;
- default:
- return sv.includes(lowV);
- }
- });
- });
- if (targetSort.fieldId && targetSort.direction) {
- list.sort((a, b) => {
- let va = a[targetSort.fieldId], vb = b[targetSort.fieldId];
- if (va === vb) return 0;
- const res = va > vb ? 1 : -1;
- return targetSort.direction === "asc" ? res : -res;
- });
- }
- state._appliedHash = currentHash;
- state.sortConfig = targetSort;
- state.list = list;
- state._internalUpdate = false;
- };
- container.showColumnMenu = (field, e) => {
- e.stopPropagation();
- const btn = e.currentTarget, menu = container.querySelector(".dt-column-menu");
- const type = field.type || "text";
- state.activeModes = MODE_MAP[type] || (["boolean", "switch", "checkbox", "radio"].includes(type) ? [] : MODE_MAP.text);
- if (!state.filterConfig[field.id]) {
- state.filterConfig[field.id] = { mode: state.activeModes[0] || "contains", value: "", selectedValues: [] };
- }
- state.activeField = field;
- state.activeFieldId = field.id;
- menu.style.display = "block";
- const cellNode = btn.closest(".dt-cell");
- const rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
- const menuWidth = menu.offsetWidth || 260;
- let leftPos = rect.right - rootRect.left - menuWidth;
- if (leftPos < 0) leftPos = Math.max(0, rect.left - rootRect.left);
- menu.style.left = leftPos + "px";
- menu.style.top = rect.bottom - rootRect.top + 5 + "px";
- const onGlobalClick = (ev) => {
- if (menu.contains(ev.target) || btn.contains(ev.target)) return;
- container.hideColumnMenu();
- container.applySortFilter();
- document.removeEventListener("mousedown", onGlobalClick);
};
- document.addEventListener("mousedown", onGlobalClick);
- setTimeout(() => {
+ };
+ const createScrollManager = (container, state, onRenderedListChange) => {
+ const vs = globalThis.VirtualScroll({ itemHeight: 40 });
+ let scrollEl = null;
+ const refresh = (isLayoutChange = false) => {
+ if (!scrollEl) return;
+ const res = vs.calc(scrollEl, state.list);
+ if (res) {
+ if (!isLayoutChange && state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) return;
+ Object.assign(state, { prevHeight: res.prevHeight, postHeight: res.postHeight, _listStartIndex: res.listStartIndex, _renderedList: res.renderedList });
+ onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length, isLayoutChange);
+ }
+ };
+ return {
+ init: () => {
+ scrollEl = container.querySelector(".dt-main");
+ },
+ reset: (list) => {
+ state._listStartIndex = 0;
+ vs.reset(list, scrollEl || container);
+ if (state.list === list) vs.init(list, () => refresh(true));
+ },
+ refresh,
+ onScroll: () => refresh(false)
+ };
+ };
+ const createSelectionManager = (container, state) => {
+ let activeBounds = null;
+ let startCell = null;
+ let multiSelections = [];
+ const isCellSelected = (r, c) => {
+ if (activeBounds && r >= activeBounds.minRow && r <= activeBounds.maxRow && c >= activeBounds.minCol && c <= activeBounds.maxCol) return true;
+ return multiSelections.some((s) => r >= s.minRow && r <= s.maxRow && c >= s.minCol && c <= s.maxCol);
+ };
+ let lastHadSelection = false;
+ const applySelectionUI = () => {
+ if (globalThis.__DT_FEATURES__ && !globalThis.__DT_FEATURES__.selection) return;
+ let boundMinRow = Infinity, boundMaxRow = -Infinity;
+ if (activeBounds) {
+ boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
+ boundMaxRow = Math.max(boundMaxRow, activeBounds.maxRow);
+ }
+ multiSelections.forEach((s) => {
+ boundMinRow = Math.min(boundMinRow, s.minRow);
+ boundMaxRow = Math.max(boundMaxRow, s.maxRow);
+ });
+ const hasSelection = boundMinRow !== Infinity;
+ if (!hasSelection && !lastHadSelection) return;
+ lastHadSelection = hasSelection;
+ const body = container.querySelector(".dt-body");
+ if (!body) return;
+ const rowNodes = body.querySelectorAll(".dt-body-row");
+ rowNodes.forEach((rowNode) => {
+ var _a;
+ const absoluteRow = (((_a = rowNode._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex;
+ const cells = rowNode.querySelectorAll(".dt-cell");
+ if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
+ cells.forEach((cell) => cell.classList.remove("dt-cell-selected"));
+ return;
+ }
+ cells.forEach((cell, cIdx) => {
+ if (isCellSelected(absoluteRow, cIdx)) cell.classList.add("dt-cell-selected");
+ else cell.classList.remove("dt-cell-selected");
+ });
+ });
+ };
+ const updateStatus = () => {
+ let count = 0;
+ if (activeBounds) count += activeBounds.maxRow - activeBounds.minRow + 1;
+ multiSelections.forEach((s) => count += s.maxRow - s.minRow + 1);
+ state.selectedRowCount = count;
+ };
+ const clearAllActive = (keepSelection = false) => {
+ if (!keepSelection) {
+ activeBounds = null;
+ startCell = null;
+ multiSelections = [];
+ applySelectionUI();
+ updateStatus();
+ }
+ };
+ const startSelect = (row, col, e) => {
+ const alreadySelected = isCellSelected(row, col);
+ const isRange = activeBounds && (activeBounds.minRow !== activeBounds.maxRow || activeBounds.minCol !== activeBounds.maxCol) || multiSelections.length > 0;
+ if (e.shiftKey && startCell) {
+ activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) };
+ } else {
+ if (alreadySelected && !e.ctrlKey && !e.metaKey) {
+ if (!isRange) container._potentialCancel = { row, col };
+ } else {
+ if (!e.ctrlKey && !e.metaKey) clearAllActive();
+ else if (activeBounds && !alreadySelected) multiSelections.push(activeBounds);
+ startCell = { row, col };
+ activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col };
+ }
+ state.isSelecting = true;
+ }
+ applySelectionUI();
+ updateStatus();
+ container.focus();
+ };
+ const updateSelect = (row, col) => {
+ if (state.isSelecting && startCell) {
+ activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) };
+ container._potentialCancel = null;
+ applySelectionUI();
+ updateStatus();
+ }
+ };
+ const endSelect = () => {
+ if (container._potentialCancel) {
+ const { row, col } = container._potentialCancel;
+ if (isCellSelected(row, col)) clearAllActive();
+ container._potentialCancel = null;
+ }
+ state.isSelecting = false;
+ };
+ const getSelectionBounds = () => {
+ if (!activeBounds) return null;
+ let minRow = activeBounds.minRow, maxRow = activeBounds.maxRow, minCol = activeBounds.minCol, maxCol = activeBounds.maxCol;
+ multiSelections.forEach((s) => {
+ minRow = Math.min(minRow, s.minRow);
+ maxRow = Math.max(maxRow, s.maxRow);
+ minCol = Math.min(minCol, s.minCol);
+ maxCol = Math.max(maxCol, s.maxCol);
+ });
+ return { minRow, maxRow, minCol, maxCol };
+ };
+ const copy = async () => {
+ const bounds = getSelectionBounds();
+ if (!bounds) return;
+ const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map((row) => {
+ return state.fields.slice(bounds.minCol, bounds.maxCol + 1).map((f) => {
+ let val = String(row[f.id] ?? "");
+ if (val.includes(" ") || val.includes("\n") || val.includes('"')) val = '"' + val.replace(/"/g, '""') + '"';
+ return val;
+ }).join(" ");
+ }).join("\n");
+ await navigator.clipboard.writeText(text);
+ };
+ const paste = async () => {
+ try {
+ const text = await navigator.clipboard.readText();
+ if (!text) return;
+ const bounds = getSelectionBounds();
+ if (!bounds) return;
+ const rows = text.split(/\r?\n/).filter((line) => line.length > 0).map((line) => {
+ const cells = [];
+ let current = "", inQuotes = false;
+ for (let i = 0; i < line.length; i++) {
+ const char = line[i];
+ if (char === '"') {
+ if (inQuotes && line[i + 1] === '"') {
+ current += '"';
+ i++;
+ } else inQuotes = !inQuotes;
+ } else if (char === " " && !inQuotes) {
+ cells.push(current);
+ current = "";
+ } else current += char;
+ }
+ cells.push(current);
+ return cells;
+ });
+ const { minRow: startRow, minCol: startCol, maxRow, maxCol } = bounds;
+ const body = container.querySelector(".dt-body");
+ const rowNodes = body ? Array.from(body.childNodes).filter((n) => {
+ var _a;
+ return (_a = n.classList) == null ? void 0 : _a.contains("dt-body-row");
+ }) : [];
+ let anyRowChanged = false;
+ rows.forEach((rowData, rOffset) => {
+ const rIdx = startRow + rOffset;
+ if (rIdx > maxRow || rIdx >= state.list.length) return;
+ const rowItem = state.list[rIdx];
+ let rowChanged = false;
+ rowData.forEach((cellData, cOffset) => {
+ const cIdx = startCol + cOffset;
+ if (cIdx > maxCol || cIdx >= state.fields.length) return;
+ const field = state.fields[cIdx];
+ rowItem[field.id] = cellData;
+ rowChanged = true;
+ });
+ if (rowChanged) anyRowChanged = true;
+ });
+ if (anyRowChanged) state.list = [...state.list];
+ } catch (err) {
+ console.error("Paste Error:", err);
+ }
+ };
+ return { applySelectionUI, clearAllActive, startSelect, updateSelect, endSelect, getSelectionBounds, copy, paste };
+ };
+ globalThis.Component.register("DataTable", (container) => {
+ if (!container.state) container.state = globalThis.NewState({});
+ const state = container.state;
+ Object.assign(state, {
+ list: [],
+ fields: [],
+ _renderedList: [],
+ prevHeight: 0,
+ postHeight: 0,
+ _listStartIndex: 0,
+ selectedRowCount: 0,
+ _originalList: [],
+ sortConfig: { fieldId: null, direction: null },
+ filterConfig: {},
+ activeFieldId: null,
+ activeField: null,
+ activeModes: [],
+ _columnStats: {},
+ _internalUpdate: false,
+ _appliedHash: "",
+ _fieldsDirty: false,
+ _masterCellNodes: null,
+ isDirty: false,
+ isBulkEdit: null
+ });
+ const perf = createPerfMonitor();
+ state.perf = perf.stats;
+ const selection = createSelectionManager(container, state);
+ const scroll = createScrollManager(container, state, () => selection.applySelectionUI());
+ const menuNode = container.querySelector(".dt-column-menu");
+ if (menuNode) menuNode._thisObj = container;
+ container.onColumnResizing = (field, e) => container.style.setProperty(`--w-${field.id}`, e.detail.newSize + "px");
+ container.onColumnResize = (field, e) => {
+ const idx = state.fields.findIndex((f) => f.id === field.id);
+ if (idx !== -1) {
+ state.fields[idx].width = e.detail.newSize;
+ state.fields = [...state.fields];
+ }
+ };
+ let _editorOverlay, currentEditingNode = null;
+ container.format = (val, field) => {
var _a;
- return (_a = menu.querySelector("input")) == null ? void 0 : _a.focus();
- }, 50);
- };
- container.toggleSelectedValue = (val) => {
- const filter = state.filterConfig[state.activeFieldId];
- if (!filter) return;
- const idx = filter.selectedValues.indexOf(val);
- if (idx === -1) filter.selectedValues.push(val);
- else filter.selectedValues.splice(idx, 1);
- state.filterConfig = { ...state.filterConfig };
- container.applySortFilter();
- };
- container.filterOnlyThis = (val) => {
- state.filterConfig[state.activeFieldId] = { mode: "contains", value: "", selectedValues: [String(val)] };
- state.filterConfig = { ...state.filterConfig };
- container.applySortFilter();
- };
- container.hideColumnMenu = () => {
- const menu = container.querySelector(".dt-column-menu");
- if (menu) menu.style.display = "none";
- };
- container.setSort = (dir) => {
- const newDir = state.sortConfig.direction === dir && state.sortConfig.fieldId === state.activeFieldId ? null : dir;
- container.applySortFilter({ sort: newDir });
- };
- container.clearColumnSettings = () => {
- if (state.activeFieldId) {
- delete state.filterConfig[state.activeFieldId];
+ if (field.formatter) return field.formatter(val, field);
+ const typeInfo = DataTableConfig._fieldTypes.get(((_a = field.settings) == null ? void 0 : _a.formType) || field.type || "text");
+ if (typeInfo && typeInfo.formatter) return typeInfo.formatter(val, field);
+ return val == null ? "" : typeof val === "object" ? JSON.stringify(val) : String(val);
+ };
+ container.onScroll = () => {
+ perf.onScroll();
+ scroll.refresh();
+ container.hideColumnMenu();
+ const prev = container.querySelector(".dt-spacer-prev"), post = container.querySelector(".dt-spacer-post");
+ if (prev) {
+ prev.style.height = (state.prevHeight || 0) + "px";
+ prev.style.display = state.prevHeight > 0 ? "block" : "none";
+ }
+ if (post) {
+ post.style.height = (state.postHeight || 0) + "px";
+ post.style.display = state.postHeight > 0 ? "block" : "none";
+ }
+ };
+ container.applySortFilter = (options = {}) => {
+ if (state._internalUpdate) return;
+ const targetFilters = { ...state.filterConfig, ...options.filters || {} };
+ const targetSort = options.sort !== void 0 ? options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null } : state.sortConfig;
+ let filtered = [...state._originalList];
+ Object.entries(targetFilters).forEach(([fId, cfg]) => {
+ if (!cfg.value && (!cfg.selectedValues || cfg.selectedValues.length === 0)) return;
+ filtered = filtered.filter((item) => {
+ var _a;
+ const val = item[fId];
+ if (((_a = cfg.selectedValues) == null ? void 0 : _a.length) > 0) return cfg.selectedValues.includes(String(val));
+ const search = String(cfg.value).toLowerCase();
+ const target = String(val ?? "").toLowerCase();
+ switch (cfg.mode) {
+ case "contains":
+ return target.includes(search);
+ case "equals":
+ return target === search;
+ case "starts":
+ return target.startsWith(search);
+ case "ends":
+ return target.endsWith(search);
+ case "=":
+ return Number(val) === Number(cfg.value);
+ case ">":
+ return Number(val) > Number(cfg.value);
+ case "<":
+ return Number(val) < Number(cfg.value);
+ case "between":
+ return Number(val) >= Number(cfg.value) && Number(val) <= Number(cfg.value2);
+ default:
+ return true;
+ }
+ });
+ });
+ if (targetSort.fieldId && targetSort.direction) {
+ const fId = targetSort.fieldId;
+ const dir = targetSort.direction === "asc" ? 1 : -1;
+ filtered.sort((a, b) => {
+ if (a[fId] == b[fId]) return 0;
+ return a[fId] > b[fId] ? dir : -dir;
+ });
+ }
+ state._internalUpdate = true;
+ state.filterConfig = targetFilters;
+ state.sortConfig = targetSort;
+ state.list = filtered;
+ state._internalUpdate = false;
+ };
+ container.showColumnMenu = (field, event) => {
+ var _a;
+ const btn = event.currentTarget, menu = container.querySelector(".dt-column-menu");
+ const type = ((_a = field.settings) == null ? void 0 : _a.formType) || field.type || "text";
+ state.activeModes = MODE_MAP[type] || (["boolean", "switch", "checkbox", "radio"].includes(type) ? [] : MODE_MAP.text);
+ if (!state.filterConfig[field.id]) state.filterConfig[field.id] = { mode: state.activeModes[0] || "contains", value: "", selectedValues: [] };
+ state.activeField = field;
+ state.activeFieldId = field.id;
+ menu.style.display = "block";
+ const cellNode = btn.closest(".dt-cell"), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
+ const menuWidth = menu.offsetWidth || 260;
+ let leftPos = rect.right - rootRect.left - menuWidth;
+ if (leftPos < 0) leftPos = Math.max(0, rect.left - rootRect.left);
+ menu.style.left = leftPos + "px";
+ menu.style.top = rect.bottom - rootRect.top + 5 + "px";
+ const onGlobalClick = (ev) => {
+ if (menu.contains(ev.target) || btn.contains(ev.target)) return;
+ container.hideColumnMenu();
+ container.applySortFilter();
+ document.removeEventListener("mousedown", onGlobalClick);
+ };
+ document.addEventListener("mousedown", onGlobalClick);
+ setTimeout(() => {
+ var _a2;
+ return (_a2 = menu.querySelector("input")) == null ? void 0 : _a2.focus();
+ }, 50);
+ };
+ container.toggleSelectedValue = (val) => {
+ const filter = state.filterConfig[state.activeFieldId];
+ if (!filter) return;
+ const idx = filter.selectedValues.indexOf(val);
+ if (idx === -1) filter.selectedValues.push(val);
+ else filter.selectedValues.splice(idx, 1);
state.filterConfig = { ...state.filterConfig };
container.applySortFilter();
- }
- };
- container._initRow = (rowNode) => {
- var _a;
- const row = (_a = rowNode._ref) == null ? void 0 : _a.item;
- if (row && row._editingF === void 0) {
- Object.defineProperty(row, "_editingF", { set: (v) => {
- if (v === null) container.hideEditor(true);
- }, configurable: true });
- }
- Array.from(rowNode.children).forEach((cell) => {
- const fIdx = parseInt(cell.dataset.fidx);
- if (!isNaN(fIdx)) cell._refExt = { f: state.fields[fIdx], fIdx };
- });
- };
- state.__watch("fields", (fields) => {
- if (!fields) return;
- state._fieldsDirty = true;
- container.style.setProperty("--dt-grid-template", fields.map((f) => `var(--w-${f.id}, ${f.width || 150}px)`).join(" "));
- container.style.setProperty("--dt-row-width", fields.reduce((sum, f) => sum + (f.width || 150), 0) + "px");
- let leftSum = 0;
- fields.forEach((f) => {
- if (f.pinned === "left") {
- container.style.setProperty(`--l-${f.id}`, leftSum + "px");
- leftSum += f.width || 150;
+ };
+ container.filterOnlyThis = (val) => {
+ state.filterConfig[state.activeFieldId] = { mode: "contains", value: "", selectedValues: [String(val)] };
+ state.filterConfig = { ...state.filterConfig };
+ container.applySortFilter();
+ };
+ container.hideColumnMenu = () => {
+ const menu = container.querySelector(".dt-column-menu");
+ if (menu) menu.style.display = "none";
+ };
+ container.setSort = (dir) => {
+ const newDir = state.sortConfig.direction === dir && state.sortConfig.fieldId === state.activeFieldId ? null : dir;
+ container.applySortFilter({ sort: newDir });
+ };
+ container.clearColumnSettings = () => {
+ if (state.activeFieldId) {
+ delete state.filterConfig[state.activeFieldId];
+ state.filterConfig = { ...state.filterConfig };
+ container.applySortFilter();
}
- });
- let rightSum = 0;
- [...fields].reverse().forEach((f) => {
- if (f.pinned === "right") {
- container.style.setProperty(`--r-${f.id}`, rightSum + "px");
- rightSum += f.width || 150;
- }
- });
- });
- state.__watch("list", (list) => {
- var _a;
- if (state._fieldsDirty) {
- state._fieldsDirty = false;
- const fieldTemplate = (_a = container.querySelector('.dt-body template[index="rIdx"]')) == null ? void 0 : _a.content.querySelector('template[as="f"]');
- if (fieldTemplate) {
- const masters = state._masterCellNodes || (state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map((n) => n.cloneNode(true)));
- fieldTemplate.removeAttribute("$each");
- fieldTemplate.setAttribute("$if", "true");
- fieldTemplate.content.textContent = "";
- state.fields.forEach((f, fIdx) => masters.forEach((master) => {
- const clone = master.cloneNode(true);
- if (clone.nodeType === 1) clone.dataset.fidx = fIdx;
- fieldTemplate.content.appendChild(clone);
- }));
- }
- }
- if (!state._internalUpdate) {
- state._originalList = [...list || []];
- setTimeout(() => {
- const stats = {};
- state.fields.forEach((f) => {
- const counts = {};
- state._originalList.forEach((item) => {
- const val = item[f.id], key = val == null || val === "" ? "" : String(val);
- counts[key] = (counts[key] || 0) + 1;
- });
- stats[f.id] = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([val, count]) => ({ val, count }));
- });
- state._columnStats = stats;
- }, 200);
- }
- scroll.init();
- scroll.reset(list);
- });
- container.editCell = (row, field, cellNode) => {
- const overlay = container.querySelector(".dt-editor-overlay"), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
- currentEditingNode = cellNode;
- Object.assign(overlay.style, {
- display: "flex",
- left: rect.left - rootRect.left + "px",
- top: rect.top - rootRect.top + "px",
- width: (field.type === "textarea" || field.type === "TagsInput" ? Math.max(rect.width, 300) : rect.width) + "px",
- height: field.type === "textarea" || field.type === "TagsInput" ? "auto" : rect.height + "px"
- });
- State.editingSchema = [{ ...field, name: field.id, label: "" }];
- State.editingData = row;
- setTimeout(() => {
+ };
+ container._initRow = (rowNode) => {
var _a;
- return (_a = overlay.querySelector("input, textarea, select, .form-control")) == null ? void 0 : _a.focus();
- }, 30);
- };
- container.hideEditor = (save = true) => {
- if (!_editorOverlay) _editorOverlay = container.querySelector(".dt-editor-overlay");
- if (!_editorOverlay || _editorOverlay.style.display === "none") return;
- if (save) {
- const input = _editorOverlay.querySelector("input:focus, select:focus, textarea:focus");
- if (input) input.dispatchEvent(new Event(input.type === "number" || input.tagName === "SELECT" ? "change" : "input", { bubbles: true }));
- if (currentEditingNode) RefreshState(currentEditingNode);
- }
- _editorOverlay.style.display = "none";
- State.editingSchema = State.editingData = currentEditingNode = null;
- container.focus();
- };
- container.onMainMouseDown = (e) => {
- var _a;
- const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
- if (!row || row.classList.contains("dt-header-row")) return;
- const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
- const rIdx = ((_a = row._ref) == null ? void 0 : _a.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
- selection.startSelect(rIdx + state._listStartIndex, fIdx, e);
- };
- container.onMainMouseOver = (e) => {
- var _a;
- if (state.isSelecting) {
+ const row = (_a = rowNode._ref) == null ? void 0 : _a.item;
+ if (row && row._editingF === void 0) {
+ Object.defineProperty(row, "_editingF", { set: (v) => {
+ if (v === null) container.hideEditor(true);
+ }, configurable: true });
+ }
+ Array.from(rowNode.children).forEach((cell) => {
+ const fIdx = parseInt(cell.dataset.fidx);
+ if (!isNaN(fIdx)) cell._ref = { ...cell._ref || rowNode._ref, f: state.fields[fIdx], fIdx };
+ });
+ };
+ state.__watch("fields", (fields) => {
+ if (!fields) return;
+ state._fieldsDirty = true;
+ state._masterCellNodes = null;
+ container.style.setProperty("--dt-grid-template", fields.map((f) => {
+ var _a;
+ return `var(--w-${f.id}, ${((_a = f.settings) == null ? void 0 : _a.width) || f.width || 150}px)`;
+ }).join(" "));
+ container.style.setProperty("--dt-row-width", fields.reduce((sum, f) => {
+ var _a;
+ return sum + (((_a = f.settings) == null ? void 0 : _a.width) || f.width || 150);
+ }, 0) + "px");
+ let leftSum = 0;
+ fields.forEach((f) => {
+ var _a, _b;
+ const pinned = ((_a = f.settings) == null ? void 0 : _a.pinned) || f.pinned;
+ if (pinned === "left") {
+ container.style.setProperty(`--l-${f.id}`, leftSum + "px");
+ leftSum += ((_b = f.settings) == null ? void 0 : _b.width) || f.width || 150;
+ }
+ });
+ let rightSum = 0;
+ [...fields].reverse().forEach((f) => {
+ var _a, _b;
+ const pinned = ((_a = f.settings) == null ? void 0 : _a.pinned) || f.pinned;
+ if (pinned === "right") {
+ container.style.setProperty(`--r-${f.id}`, rightSum + "px");
+ rightSum += ((_b = f.settings) == null ? void 0 : _b.width) || f.width || 150;
+ }
+ });
+ });
+ state.__watch("list", (list) => {
+ var _a;
+ if (state._fieldsDirty) {
+ state._fieldsDirty = false;
+ const fieldTemplate = (_a = container.querySelector('.dt-body template[index="rIdx"]')) == null ? void 0 : _a.content.querySelector('template[as="f"]');
+ if (fieldTemplate) {
+ const masters = state._masterCellNodes || (state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map((n) => n.cloneNode(true)));
+ fieldTemplate.removeAttribute("$each");
+ fieldTemplate.setAttribute("$if", "true");
+ fieldTemplate.content.textContent = "";
+ state.fields.forEach((f, fIdx) => masters.forEach((master) => {
+ var _a2;
+ const clone = master.cloneNode(true);
+ if (clone.nodeType === 1) {
+ clone.dataset.fidx = fIdx;
+ const pinned = ((_a2 = f.settings) == null ? void 0 : _a2.pinned) || f.pinned;
+ if (pinned) {
+ clone.classList.add("pinned-" + pinned);
+ clone.style.position = "sticky";
+ clone.style.zIndex = "1";
+ clone.style.backgroundColor = "inherit";
+ if (pinned === "left") {
+ clone.style.left = `var(--l-${f.id})`;
+ clone.style.borderRight = "1px solid var(--bs-border-color)";
+ clone.style.boxShadow = "2px 0 5px -2px rgba(0,0,0,0.1)";
+ } else {
+ clone.style.right = `var(--r-${f.id})`;
+ clone.style.borderLeft = "1px solid var(--bs-border-color)";
+ clone.style.boxShadow = "-2px 0 5px -2px rgba(0,0,0,0.1)";
+ }
+ }
+ }
+ fieldTemplate.content.appendChild(clone);
+ }));
+ }
+ }
+ if (!state._internalUpdate) {
+ state._originalList = [...list || []];
+ setTimeout(() => {
+ const stats = {};
+ state.fields.forEach((f) => {
+ const counts = {};
+ state._originalList.forEach((item) => {
+ const val = item[f.id], key = val == null || val === "" ? "" : String(val);
+ counts[key] = (counts[key] || 0) + 1;
+ });
+ stats[f.id] = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([val, count]) => ({ val, count }));
+ });
+ state._columnStats = stats;
+ }, 200);
+ }
+ scroll.init();
+ scroll.reset(list);
+ });
+ container.editCell = (row, field, cellNode) => {
+ var _a, _b;
+ const overlay = container.querySelector(".dt-editor-overlay"), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
+ currentEditingNode = cellNode;
+ const formType = ((_a = field.settings) == null ? void 0 : _a.formType) || field.type || "text";
+ const form = overlay.querySelector("AutoForm");
+ if (form) {
+ form.data = row;
+ form.state.schema = [{ ...field, type: formType, options: ((_b = field.settings) == null ? void 0 : _b.options) || field.options, name: field.id, label: "" }];
+ }
+ Object.assign(overlay.style, {
+ display: "flex",
+ left: rect.left - rootRect.left + "px",
+ top: rect.top - rootRect.top + "px",
+ width: (formType === "textarea" || formType === "TagsInput" ? Math.max(rect.width, 300) : rect.width) + "px",
+ height: formType === "textarea" || formType === "TagsInput" ? "auto" : rect.height + "px"
+ });
+ setTimeout(() => {
+ var _a2;
+ return (_a2 = overlay.querySelector("input, textarea, select, .form-control")) == null ? void 0 : _a2.focus();
+ }, 30);
+ };
+ container.hideEditor = (save = true) => {
+ if (!_editorOverlay) _editorOverlay = container.querySelector(".dt-editor-overlay");
+ if (!_editorOverlay || _editorOverlay.style.display === "none") return;
+ const form = _editorOverlay.querySelector("AutoForm");
+ if (save && form && form.data) {
+ const input = _editorOverlay.querySelector("input:focus, select:focus, textarea:focus");
+ if (input) input.dispatchEvent(new Event(input.type === "number" || input.tagName === "SELECT" ? "change" : "input", { bubbles: true }));
+ const schema = form.state.schema || [];
+ schema.forEach((field) => {
+ var _a, _b;
+ const row = (_b = (_a = currentEditingNode == null ? void 0 : currentEditingNode.closest(".dt-row")) == null ? void 0 : _a._ref) == null ? void 0 : _b.item;
+ if (row) row[field.name] = form.data[field.name];
+ });
+ if (state.isBulkEdit) {
+ const { minRow, maxRow, fIdx } = state.isBulkEdit;
+ const field = state.fields[fIdx];
+ const newValue = form.data[field.id];
+ for (let i = minRow; i <= maxRow; i++) {
+ if (state.list[i]) state.list[i][field.id] = newValue;
+ }
+ }
+ state.list = [...state.list];
+ state.isDirty = true;
+ }
+ _editorOverlay.style.display = "none";
+ if (form) {
+ form.state.schema = [];
+ form.data = null;
+ }
+ currentEditingNode = null;
+ state.isBulkEdit = null;
+ container.focus();
+ };
+ container.onMainMouseDown = (e) => {
+ var _a;
+ const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
+ if (!row || row.classList.contains("dt-header-row")) return;
+ const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
+ const rIdx = ((_a = row._ref) == null ? void 0 : _a.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
+ selection.startSelect(rIdx + state._listStartIndex, fIdx, e);
+ };
+ container.onMainMouseOver = (e) => {
+ var _a;
+ if (state.isSelecting) {
+ const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
+ if (row && !row.classList.contains("dt-header-row")) {
+ const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
+ const rIdx = ((_a = row._ref) == null ? void 0 : _a.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
+ selection.updateSelect(rIdx + state._listStartIndex, fIdx);
+ }
+ }
+ };
+ container.onMainDblClick = (e) => {
+ var _a, _b, _c;
const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
if (row && !row.classList.contains("dt-header-row")) {
- const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
- const rIdx = ((_a = row._ref) == null ? void 0 : _a.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
- selection.updateSelect(rIdx + state._listStartIndex, fIdx);
+ const item = (_a = row._ref) == null ? void 0 : _a.item, fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
+ const rIdx = ((_b = row._ref) == null ? void 0 : _b.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
+ const absoluteRow = rIdx + state._listStartIndex;
+ if (item && state.fields[fIdx]) {
+ const bounds = selection.getSelectionBounds();
+ if (bounds && absoluteRow >= bounds.minRow && absoluteRow <= bounds.maxRow && fIdx >= bounds.minCol && fIdx <= bounds.maxCol) {
+ const affectedRows = bounds.maxRow - bounds.minRow + 1;
+ if (affectedRows > 1) {
+ state.isBulkEdit = { ...bounds, fIdx };
+ if ((_c = globalThis.UI) == null ? void 0 : _c.toast) globalThis.UI.toast(`Bulk Edit: Updating ${affectedRows} rows in column "${state.fields[fIdx].name}"`, { type: "warning" });
+ }
+ }
+ container.editCell(item, state.fields[fIdx], cell);
+ }
}
- }
- };
- container.onMainDblClick = (e) => {
- var _a;
- const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
- if (row && !row.classList.contains("dt-header-row")) {
- const item = (_a = row._ref) == null ? void 0 : _a.item, fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
- if (item && state.fields[fIdx]) container.editCell(item, state.fields[fIdx], cell);
- }
- };
- window.addEventListener("mouseup", selection.endSelect);
- document.addEventListener("mousedown", (e) => {
- const overlay = container.querySelector(".dt-editor-overlay");
- if ((overlay == null ? void 0 : overlay.style.display) !== "none" && !overlay.contains(e.target)) container.hideEditor(true);
- if (!container.contains(e.target) && !(overlay == null ? void 0 : overlay.contains(e.target))) selection.clearAllActive();
- });
- state._MODE_ICONS = MODE_ICONS;
-}, Util.makeDom(
- /*html*/
- `
-
+ };
+ container.addRow = () => {
+ const newRow = {};
+ state.fields.forEach((f) => newRow[f.id] = "");
+ state._originalList.push(newRow);
+ state.list = [...state._originalList];
+ state.isDirty = true;
+ setTimeout(() => {
+ scroll.reset(state.list);
+ container.querySelector(".dt-main").scrollTop = container.querySelector(".dt-main").scrollHeight;
+ }, 50);
+ };
+ container.deleteSelectedRow = async () => {
+ const bounds = selection.getSelectionBounds();
+ if (!bounds) return;
+ const count = bounds.maxRow - bounds.minRow + 1;
+ if (await globalThis.UI.confirm(`Are you sure you want to delete ${count} row(s)?`)) {
+ const rMin = bounds.minRow, rMax = bounds.maxRow;
+ const removedItems = state.list.slice(rMin, rMax + 1);
+ state.list = state.list.filter((_, i) => !(i >= rMin && i <= rMax));
+ state._originalList = state._originalList.filter((item) => !removedItems.includes(item));
+ state.isDirty = true;
+ selection.clearAllActive();
+ container.dispatchEvent(new CustomEvent("remove", { detail: { items: removedItems } }));
+ }
+ };
+ container.saveChanges = () => {
+ container.dispatchEvent(new CustomEvent("save", { detail: { list: state._originalList, fields: state.fields } }));
+ state.isDirty = false;
+ };
+ const getFieldSchema = () => {
+ const types = globalThis.DataTable.getFieldTypes();
+ const baseSchema = [
+ { name: "id", label: "Field ID", type: "text", setting: { required: true, placeholder: "e.g. user_name" } },
+ { name: "name", label: "Display Name", type: "text", setting: { required: true, placeholder: "e.g. 用户名" } },
+ { name: "user_type", label: "Field Type", type: "select", options: types.map((t) => ({ label: t.label, value: t.value })) }
+ ];
+ const dynamicSchema = types.reduce((acc, t) => acc.concat(t.schema || []), []);
+ return baseSchema.concat(dynamicSchema, [{ name: "isIndex", label: "Index", type: "switch" }, { name: "memo", label: "Memo", type: "text" }]);
+ };
+ const parseOptionsStr = (str) => {
+ if (!str) return void 0;
+ return str.split("\n").map((s) => s.trim()).filter(Boolean).map((line) => {
+ const idx = line.indexOf(":");
+ if (idx > -1) return { label: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() };
+ return line;
+ });
+ };
+ const formatOptionsStr = (opts) => {
+ if (!opts) return "";
+ return opts.map((o) => typeof o === "object" ? `${o.label}:${o.value}` : o).join("\n");
+ };
+ container.addField = async () => {
+ container.hideColumnMenu();
+ const data = globalThis.NewState({ id: "c" + Date.now().toString().slice(-4), name: "New Field", user_type: "text", decimals: 0, isIndex: false, memo: "", options_str: "" });
+ const d = container.querySelector(`Dialog[id="${container.id}_field_dialog"]`);
+ if (!d) return;
+ Object.assign(d.state, { title: "Add Field", buttons: ["Cancel", "Save"] });
+ const form = d.querySelector("AutoForm");
+ if (form) {
+ form.data = data;
+ form.state.schema = getFieldSchema();
+ }
+ d.show();
+ const result = await new Promise((resolve) => d.addEventListener("change", (e) => resolve(d.result), { once: true }));
+ if (result === 2) {
+ const typeInfo = globalThis.DataTable.getFieldTypes().find((t) => t.value === data.user_type);
+ let dbType = (typeInfo == null ? void 0 : typeInfo.typeForDB) || "v1024";
+ if (data.user_type === "number") dbType = data.decimals > 0 ? "ff" : "bi";
+ const field = { id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } };
+ state.fields = [...state.fields, field];
+ state.isDirty = true;
+ container.dispatchEvent(new CustomEvent("savefields", { detail: state.fields }));
+ state.list = [...state.list];
+ }
+ };
+ container.editField = async () => {
+ if (!state.activeField) return;
+ container.hideColumnMenu();
+ const f = state.activeField;
+ const s = f.settings || {};
+ const data = globalThis.NewState({ id: f.id, name: f.name, memo: f.memo || "", isIndex: !!f.isIndex, user_type: s.formType || "text", decimals: s.decimals || 0, prefix: s.prefix || "", suffix: s.suffix || "", thousandSep: !!s.thousandSep, labelOn: s.labelOn || "", labelOff: s.labelOff || "", format: s.format || "", placeholder: s.placeholder || "", options_str: formatOptionsStr(s.options) });
+ const d = container.querySelector(`Dialog[id="${container.id}_field_dialog"]`);
+ if (!d) return;
+ Object.assign(d.state, { title: "Edit Field", buttons: ["Cancel", "Save"] });
+ const form = d.querySelector("AutoForm");
+ if (form) {
+ form.data = data;
+ form.state.schema = getFieldSchema();
+ }
+ d.show();
+ const result = await new Promise((resolve) => d.addEventListener("change", (e) => resolve(d.result), { once: true }));
+ if (result === 2) {
+ const idx = state.fields.findIndex((item) => item.id === f.id);
+ if (idx !== -1) {
+ const typeInfo = globalThis.DataTable.getFieldTypes().find((t) => t.value === data.user_type);
+ let dbType = (typeInfo == null ? void 0 : typeInfo.typeForDB) || "v1024";
+ if (data.user_type === "number") dbType = data.decimals > 0 ? "ff" : "bi";
+ const updatedField = { ...f, id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { ...f.settings, formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } };
+ state.fields[idx] = updatedField;
+ state.fields = [...state.fields];
+ state.isDirty = true;
+ container.dispatchEvent(new CustomEvent("savefields", { detail: state.fields }));
+ state.list = [...state.list];
+ }
+ }
+ };
+ container.deleteField = async () => {
+ if (!state.activeField) return;
+ container.hideColumnMenu();
+ if (await globalThis.UI.confirm(`Are you sure you want to delete field "${state.activeField.name}"?`)) {
+ const idx = state.fields.findIndex((f) => f.id === state.activeField.id);
+ if (idx !== -1) {
+ state.fields.splice(idx, 1);
+ state.fields = [...state.fields];
+ state.isDirty = true;
+ container.dispatchEvent(new CustomEvent("savefields", { detail: state.fields }));
+ state.list = [...state.list];
+ }
+ }
+ };
+ window.addEventListener("mouseup", selection.endSelect);
+ document.addEventListener("mousedown", (e) => {
+ const overlay = container.querySelector(".dt-editor-overlay");
+ const menu = container.querySelector(".dt-column-menu");
+ if ((overlay == null ? void 0 : overlay.style.display) !== "none" && !overlay.contains(e.target)) container.hideEditor(true);
+ if (!container.contains(e.target) && !(overlay == null ? void 0 : overlay.contains(e.target)) && !(menu == null ? void 0 : menu.contains(e.target))) selection.clearAllActive();
+ });
+ state._MODE_ICONS = MODE_ICONS;
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
TOP FREQUENT VALUES
-
-
-
-
+
+
TOP FREQUENT VALUES
+
+
+
+
-
- Clear Filter
-
-
-
+
+ Clear Filter
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
`
-), Util.makeDom(
- /*html*/
- `
+ ), globalThis.Util.makeDom(
+ /*html*/
+ `
`
-));
+ ));
+ globalThis.DataTable = DataTableConfig;
+});
diff --git a/dist/datatable.min.js b/dist/datatable.min.js
index 49143a0..3e0cb5e 100644
--- a/dist/datatable.min.js
+++ b/dist/datatable.min.js
@@ -1,80 +1 @@
-import{Component as e,NewState as t,RefreshState as i,Util as o}from"@web/state";import{VirtualScroll as n,State as r}from"@web/base";const l={text:["contains","equals","starts","ends"],textarea:["contains","equals","starts","ends"],number:["=",">","<","between"],date:["=",">","<","between"],select:["contains","equals"],TagsInput:["contains","equals","starts","ends"]},s={contains:"bi-search",equals:"bi-distribute-vertical",starts:"bi-align-start",ends:"bi-align-end","=":"bi-calculator",">":"bi-chevron-right","<":"bi-chevron-left",between:"bi-arrows-expand"};e.register("DataTable",e=>{e.state||(e.state=t({}));const o=e.state;Object.assign(o,{list:[],fields:[],_renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selectedRowCount:0,_originalList:[],sortConfig:{fieldId:null,direction:null},filterConfig:{},activeFieldId:null,activeField:null,activeModes:[],_columnStats:{},_internalUpdate:!1,_appliedHash:"",_fieldsDirty:!1,_masterCellNodes:null});const a=(()=>{let e=!!window.__DT_PERF_MODE__;const t={refreshTime:0,refreshCount:0,scrollCount:0,totalNodes:0};return e&&!window.__statePerformanceTelemetry&&(window.__statePerformanceTelemetry={scanCount:0,reuseCount:0,moveCount:0}),{get stats(){return t},enable:()=>{e=!0},disable:()=>{e=!1},onScroll:()=>{e&&t.scrollCount++},startFrame:()=>{var t,i,o;return e?{start:performance.now(),scan:(null==(t=window.__statePerformanceTelemetry)?void 0:t.scanCount)||0,move:(null==(i=window.__statePerformanceTelemetry)?void 0:i.moveCount)||0,reuse:(null==(o=window.__statePerformanceTelemetry)?void 0:o.reuseCount)||0}:null},endFrame:(i,o)=>{if(!e||!i)return;t.refreshCount++,t.totalNodes+=o;const n=performance.now()-i.start;t.refreshTime+=n;const r=window.__statePerformanceTelemetry;if(r){const e=r.scanCount-i.scan,t=r.moveCount-i.move,l=r.reuseCount-i.reuse;(e>0||n>2)&&console.log(`[DataTable Frame] Time: ${n.toFixed(2)}ms, Scans: ${e}, Moves: ${t}, Reuses: ${l}, Rows: ${o}`)}}}})();o.perf=a.stats;const d=((e,t)=>{let i=null,o=null,n=[];const r=(e,t)=>!!(i&&e>=i.minRow&&e<=i.maxRow&&t>=i.minCol&&t<=i.maxCol)||n.some(i=>e>=i.minRow&&e<=i.maxRow&&t>=i.minCol&&t<=i.maxCol);let l=!1;const s=()=>{if(window.__DT_FEATURES__&&!window.__DT_FEATURES__.selection)return;let o=1/0,s=-1/0;i&&(o=Math.min(o,i.minRow),s=Math.max(s,i.maxRow)),n.forEach(e=>{o=Math.min(o,e.minRow),s=Math.max(s,e.maxRow)});const a=o!==1/0;if(!a&&!l)return;l=a;const d=e.querySelector(".dt-body");d&&d.querySelectorAll(".dt-body-row").forEach(e=>{var i;const n=((null==(i=e._ref)?void 0:i.rIdx)??-1)+t._listStartIndex,l=e.querySelectorAll(".dt-cell");!a||ns?l.forEach(e=>e.classList.remove("dt-cell-selected")):l.forEach((e,t)=>{r(n,t)?e.classList.add("dt-cell-selected"):e.classList.remove("dt-cell-selected")})})},a=()=>{let e=0;i&&(e+=i.maxRow-i.minRow+1),n.forEach(t=>e+=t.maxRow-t.minRow+1),t.selectedRowCount=e},d=(e=!1)=>{e||(i=null,o=null,n=[],s(),a())},c=()=>{if(!i)return null;let e=i.minRow,t=i.maxRow,o=i.minCol,r=i.maxCol;return n.forEach(i=>{e=Math.min(e,i.minRow),t=Math.max(t,i.maxRow),o=Math.min(o,i.minCol),r=Math.max(r,i.maxCol)}),{minRow:e,maxRow:t,minCol:o,maxCol:r}};return{applySelectionUI:s,clearAllActive:d,startSelect:(l,c,m)=>{const u=r(l,c),p=i&&(i.minRow!==i.maxRow||i.minCol!==i.maxCol)||n.length>0;m.shiftKey&&o?i={minRow:Math.min(o.row,l),maxRow:Math.max(o.row,l),minCol:Math.min(o.col,c),maxCol:Math.max(o.col,c)}:(!u||m.ctrlKey||m.metaKey?(m.ctrlKey||m.metaKey?i&&!u&&n.push(i):d(),o={row:l,col:c},i={minRow:l,maxRow:l,minCol:c,maxCol:c}):p||(e._potentialCancel={row:l,col:c}),t.isSelecting=!0),s(),a(),e.focus()},updateSelect:(n,r)=>{t.isSelecting&&o&&(i={minRow:Math.min(o.row,n),maxRow:Math.max(o.row,n),minCol:Math.min(o.col,r),maxCol:Math.max(o.col,r)},e._potentialCancel=null,s(),a())},endSelect:()=>{if(e._potentialCancel){const{row:t,col:i}=e._potentialCancel;r(t,i)&&d(),e._potentialCancel=null}t.isSelecting=!1},deleteSelected:()=>{const e=c();if(!e)return;const i=e.minRow,o=e.maxRow,n=t.list.filter((e,t)=>!(t>=i&&t<=o));t.list=n,d()},getSelectionBounds:c,copy:async()=>{const e=c();if(!e)return;const i=t.list.slice(e.minRow,e.maxRow+1).map(i=>t.fields.slice(e.minCol,e.maxCol+1).map(e=>{let t=String(i[e.id]??"");return(t.includes("\t")||t.includes("\n")||t.includes('"'))&&(t='"'+t.replace(/"/g,'""')+'"'),t}).join("\t")).join("\n");await navigator.clipboard.writeText(i)},paste:async()=>{try{const i=await navigator.clipboard.readText();if(!i)return;const o=c();if(!o)return;const n=i.split(/\r?\n/).filter(e=>e.length>0).map(e=>{const t=[];let i="",o=!1;for(let n=0;n{var t;return null==(t=e.classList)?void 0:t.contains("dt-body-row")}):[];n.forEach((i,o)=>{const n=r+o;if(n>s||n>=t.list.length)return;const d=t.list[n];let c=!1;if(i.forEach((e,i)=>{const o=l+i;if(o>a||o>=t.fields.length)return;const n=t.fields[o];d[n.id]=e,c=!0}),c&&e.refreshNode){const i=m.find(e=>{var i;return((null==(i=e._ref)?void 0:i.rIdx)??-1)+t._listStartIndex===n});i&&e.refreshNode(i)}})}catch(e){console.error("Paste Error:",e)}}}})(e,o),c=((e,t,i)=>{const o=n({itemHeight:40});let r=null;const l=(e=!1)=>{if(!r)return;const n=o.calc(r,t.list);if(n){if(!e&&t.prevHeight===n.prevHeight&&t.postHeight===n.postHeight&&t._listStartIndex===n.listStartIndex&&t._renderedList.length===n.renderedList.length)return;Object.assign(t,{prevHeight:n.prevHeight,postHeight:n.postHeight,_listStartIndex:n.listStartIndex,_renderedList:n.renderedList}),null==i||i(n.renderedList.length,e)}};return{init:()=>{r=e.querySelector(".dt-main")},reset:i=>{t._listStartIndex=0,o.reset(i,r||e),t.list===i&&o.init(i,()=>l(!0))},refresh:l,onScroll:()=>l(!1)}})(e,o,()=>d.applySelectionUI()),m=e.querySelector(".dt-column-menu");m&&(m._thisObj=e),e.onColumnResizing=(t,i)=>e.style.setProperty(`--w-${t.id}`,i.detail.newSize+"px"),e.onColumnResize=(e,t)=>{const i=o.fields.findIndex(t=>t.id===e.id);-1!==i&&(o.fields[i].width=t.detail.newSize,o.fields=[...o.fields])};let u,p=null;e.format=(e,t)=>t.formatter?t.formatter(e,t):null==e?"":"object"==typeof e?JSON.stringify(e):String(e),e.onScroll=()=>{a.onScroll(),c.refresh(),e.hideColumnMenu();const t=e.querySelector(".dt-spacer-prev"),i=e.querySelector(".dt-spacer-post");t&&(t.style.height=(o.prevHeight||0)+"px",t.style.display=o.prevHeight>0?"block":"none"),i&&(i.style.height=(o.postHeight||0)+"px",i.style.display=o.postHeight>0?"block":"none")},e.applySortFilter=(e={})=>{if(o._internalUpdate)return;const t={...o.filterConfig,...e.filters||{}},i=void 0!==e.sort?e.sort?{fieldId:o.activeFieldId,direction:e.sort}:{fieldId:null,direction:null}:o.sortConfig,n=JSON.stringify({s:i,f:t});if(o._appliedHash===n&&!e.force)return;o._internalUpdate=!0;let r=[...o._originalList||[]];Object.keys(t).forEach(e=>{const i=t[e];if(!i)return;const{mode:o="contains",value:n,value2:l,selectedValues:s}=i;if((null==s?void 0:s.length)>0)return void(r=r.filter(t=>s.includes(String(t[e]??""))));if(""===n||null==n)return;const a=String(n).toLowerCase(),d=Number(n),c=Number(l);r=r.filter(t=>{const i=t[e],n=String(i??"").toLowerCase();switch(o){case"contains":default:return n.includes(a);case"equals":return n===a;case"starts":return n.startsWith(a);case"ends":return n.endsWith(a);case">":return Number(i)>d;case"<":return Number(i)=d&&Number(i)<=c}})}),i.fieldId&&i.direction&&r.sort((e,t)=>{let o=e[i.fieldId],n=t[i.fieldId];if(o===n)return 0;const r=o>n?1:-1;return"asc"===i.direction?r:-r}),o._appliedHash=n,o.sortConfig=i,o.list=r,o._internalUpdate=!1},e.showColumnMenu=(t,i)=>{i.stopPropagation();const n=i.currentTarget,r=e.querySelector(".dt-column-menu"),s=t.type||"text";o.activeModes=l[s]||(["boolean","switch","checkbox","radio"].includes(s)?[]:l.text),o.filterConfig[t.id]||(o.filterConfig[t.id]={mode:o.activeModes[0]||"contains",value:"",selectedValues:[]}),o.activeField=t,o.activeFieldId=t.id,r.style.display="block";const a=n.closest(".dt-cell").getBoundingClientRect(),d=e.getBoundingClientRect(),c=r.offsetWidth||260;let m=a.right-d.left-c;m<0&&(m=Math.max(0,a.left-d.left)),r.style.left=m+"px",r.style.top=a.bottom-d.top+5+"px";const u=t=>{r.contains(t.target)||n.contains(t.target)||(e.hideColumnMenu(),e.applySortFilter(),document.removeEventListener("mousedown",u))};document.addEventListener("mousedown",u),setTimeout(()=>{var e;return null==(e=r.querySelector("input"))?void 0:e.focus()},50)},e.toggleSelectedValue=t=>{const i=o.filterConfig[o.activeFieldId];if(!i)return;const n=i.selectedValues.indexOf(t);-1===n?i.selectedValues.push(t):i.selectedValues.splice(n,1),o.filterConfig={...o.filterConfig},e.applySortFilter()},e.filterOnlyThis=t=>{o.filterConfig[o.activeFieldId]={mode:"contains",value:"",selectedValues:[String(t)]},o.filterConfig={...o.filterConfig},e.applySortFilter()},e.hideColumnMenu=()=>{const t=e.querySelector(".dt-column-menu");t&&(t.style.display="none")},e.setSort=t=>{const i=o.sortConfig.direction===t&&o.sortConfig.fieldId===o.activeFieldId?null:t;e.applySortFilter({sort:i})},e.clearColumnSettings=()=>{o.activeFieldId&&(delete o.filterConfig[o.activeFieldId],o.filterConfig={...o.filterConfig},e.applySortFilter())},e._initRow=t=>{var i;const n=null==(i=t._ref)?void 0:i.item;n&&void 0===n._editingF&&Object.defineProperty(n,"_editingF",{set:t=>{null===t&&e.hideEditor(!0)},configurable:!0}),Array.from(t.children).forEach(e=>{const t=parseInt(e.dataset.fidx);isNaN(t)||(e._refExt={f:o.fields[t],fIdx:t})})},o.__watch("fields",t=>{if(!t)return;o._fieldsDirty=!0,e.style.setProperty("--dt-grid-template",t.map(e=>`var(--w-${e.id}, ${e.width||150}px)`).join(" ")),e.style.setProperty("--dt-row-width",t.reduce((e,t)=>e+(t.width||150),0)+"px");let i=0;t.forEach(t=>{"left"===t.pinned&&(e.style.setProperty(`--l-${t.id}`,i+"px"),i+=t.width||150)});let n=0;[...t].reverse().forEach(t=>{"right"===t.pinned&&(e.style.setProperty(`--r-${t.id}`,n+"px"),n+=t.width||150)})}),o.__watch("list",t=>{var i;if(o._fieldsDirty){o._fieldsDirty=!1;const t=null==(i=e.querySelector('.dt-body template[index="rIdx"]'))?void 0:i.content.querySelector('template[as="f"]');if(t){const e=o._masterCellNodes||(o._masterCellNodes=Array.from(t.content.childNodes).map(e=>e.cloneNode(!0)));t.removeAttribute("$each"),t.setAttribute("$if","true"),t.content.textContent="",o.fields.forEach((i,o)=>e.forEach(e=>{const i=e.cloneNode(!0);1===i.nodeType&&(i.dataset.fidx=o),t.content.appendChild(i)}))}}o._internalUpdate||(o._originalList=[...t||[]],setTimeout(()=>{const e={};o.fields.forEach(t=>{const i={};o._originalList.forEach(e=>{const o=e[t.id],n=null==o||""===o?"":String(o);i[n]=(i[n]||0)+1}),e[t.id]=Object.entries(i).sort((e,t)=>t[1]-e[1]).slice(0,20).map(([e,t])=>({val:e,count:t}))}),o._columnStats=e},200)),c.init(),c.reset(t)}),e.editCell=(t,i,o)=>{const n=e.querySelector(".dt-editor-overlay"),l=o.getBoundingClientRect(),s=e.getBoundingClientRect();p=o,Object.assign(n.style,{display:"flex",left:l.left-s.left+"px",top:l.top-s.top+"px",width:("textarea"===i.type||"TagsInput"===i.type?Math.max(l.width,300):l.width)+"px",height:"textarea"===i.type||"TagsInput"===i.type?"auto":l.height+"px"}),r.editingSchema=[{...i,name:i.id,label:""}],r.editingData=t,setTimeout(()=>{var e;return null==(e=n.querySelector("input, textarea, select, .form-control"))?void 0:e.focus()},30)},e.hideEditor=(t=!0)=>{if(u||(u=e.querySelector(".dt-editor-overlay")),u&&"none"!==u.style.display){if(t){const e=u.querySelector("input:focus, select:focus, textarea:focus");e&&e.dispatchEvent(new Event("number"===e.type||"SELECT"===e.tagName?"change":"input",{bubbles:!0})),p&&i(p)}u.style.display="none",r.editingSchema=r.editingData=p=null,e.focus()}},e.onMainMouseDown=t=>{var i;const n=t.target.closest(".dt-cell"),r=null==n?void 0:n.closest(".dt-row");if(!r||r.classList.contains("dt-header-row"))return;const l=n.dataset.fidx?parseInt(n.dataset.fidx):Array.from(r.querySelectorAll(".dt-cell")).indexOf(n),s=(null==(i=r._ref)?void 0:i.rIdx)??Array.from(e.querySelectorAll(".dt-body-row")).indexOf(r);d.startSelect(s+o._listStartIndex,l,t)},e.onMainMouseOver=t=>{var i;if(o.isSelecting){const n=t.target.closest(".dt-cell"),r=null==n?void 0:n.closest(".dt-row");if(r&&!r.classList.contains("dt-header-row")){const t=n.dataset.fidx?parseInt(n.dataset.fidx):Array.from(r.querySelectorAll(".dt-cell")).indexOf(n),l=(null==(i=r._ref)?void 0:i.rIdx)??Array.from(e.querySelectorAll(".dt-body-row")).indexOf(r);d.updateSelect(l+o._listStartIndex,t)}}},e.onMainDblClick=t=>{var i;const n=t.target.closest(".dt-cell"),r=null==n?void 0:n.closest(".dt-row");if(r&&!r.classList.contains("dt-header-row")){const t=null==(i=r._ref)?void 0:i.item,l=n.dataset.fidx?parseInt(n.dataset.fidx):Array.from(r.querySelectorAll(".dt-cell")).indexOf(n);t&&o.fields[l]&&e.editCell(t,o.fields[l],n)}},window.addEventListener("mouseup",d.endSelect),document.addEventListener("mousedown",t=>{const i=e.querySelector(".dt-editor-overlay");"none"===(null==i?void 0:i.style.display)||i.contains(t.target)||e.hideEditor(!0),e.contains(t.target)||(null==i?void 0:i.contains(t.target))||d.clearAllActive()}),o._MODE_ICONS=s},o.makeDom(`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
TOP FREQUENT VALUES
-
-
-
-
-
-
- Clear Filter
-
-
-
-
-
-
-
-`),o.makeDom("\n\n"));
+!function(e){"function"==typeof define&&define.amd?define(e):e()}(function(){"use strict";const e={text:["contains","equals","starts","ends"],textarea:["contains","equals","starts","ends"],number:["=",">","<","between"],date:["=",">","<","between"],select:["contains","equals"],TagsInput:["contains","equals","starts","ends"]},t={contains:"bi-search",equals:"bi-distribute-vertical",starts:"bi-align-start",ends:"bi-align-end","=":"bi-calculator",">":"bi-chevron-right","<":"bi-chevron-left",between:"bi-arrows-expand"},i={_fieldTypes:new Map,registerFieldType:e=>{i._fieldTypes.set(e.value,e)},getFieldTypes:()=>Array.from(i._fieldTypes.values())};i.registerFieldType({value:"text",label:"{#Text#}",typeForDB:"v4096",schema:[{name:"placeholder",label:"Placeholder",type:"text",if:'this.data.user_type=="text"'}]}),i.registerFieldType({value:"number",label:"{#Number#}",typeForDB:"ff",schema:[{name:"decimals",label:"Decimals",type:"number",setting:{min:0,max:10},if:'this.data.user_type=="number"'},{name:"prefix",label:"Prefix (e.g. $)",type:"text",if:'this.data.user_type=="number"'},{name:"suffix",label:"Suffix (e.g. %)",type:"text",if:'this.data.user_type=="number"'},{name:"thousandSep",label:"Thousand Sep",type:"switch",if:'this.data.user_type=="number"'}],formatter:(e,t)=>{if(null==e||""===e)return"";let i=Number(e);if(isNaN(i))return e;const n=t.settings||{};void 0!==n.decimals&&(i=i.toFixed(n.decimals));let l=String(i);if(n.thousandSep){const e=l.split(".");e[0]=e[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),l=e.join(".")}return(n.prefix||"")+l+(n.suffix||"")}}),i.registerFieldType({value:"select",label:"{#Single Select#}",typeForDB:"v1024",schema:[{name:"options_str",label:"Options",type:"textarea",setting:{rows:3,placeholder:"Label:Value per line"},if:'this.data.user_type=="select"'}],formatter:(e,t)=>{var i;if(null==e||""===e)return"";const n=((null==(i=t.settings)?void 0:i.options)||[]).find(t=>"object"==typeof t?t.value==e:t==e);return n?"object"==typeof n?n.label:n:e}}),i.registerFieldType({value:"checkbox",label:"{#Multi Select#}",typeForDB:"v4096",schema:[{name:"options_str",label:"Options",type:"textarea",setting:{rows:3,placeholder:"Label:Value per line"},if:'this.data.user_type=="checkbox"'}],formatter:(e,t)=>{var i;if(!Array.isArray(e))return null==e?"":String(e);const n=(null==(i=t.settings)?void 0:i.options)||[];return e.map(e=>{const t=n.find(t=>"object"==typeof t?t.value==e:t==e);return t?"object"==typeof t?t.label:t:e}).join(", ")}}),i.registerFieldType({value:"switch",label:"{#Switch#}",typeForDB:"b",schema:[{name:"labelOn",label:"Label On",type:"text",if:'this.data.user_type=="switch"'},{name:"labelOff",label:"Label Off",type:"text",if:'this.data.user_type=="switch"'}],formatter:(e,t)=>{const i=t.settings||{};return e?i.labelOn||"Yes":i.labelOff||"No"}}),i.registerFieldType({value:"datetime",label:"{#DateTime#}",typeForDB:"dt",schema:[{name:"format",label:"Format",type:"text",setting:{placeholder:"YYYY-MM-DD"},if:'this.data.user_type=="datetime"'}]}),i.registerFieldType({value:"textarea",label:"{#Long Text#}",typeForDB:"t",schema:[{name:"placeholder",label:"Placeholder",type:"text",if:'this.data.user_type=="textarea"'}]});globalThis.Component.register("DataTable",n=>{n.state||(n.state=globalThis.NewState({}));const l=n.state;Object.assign(l,{list:[],fields:[],_renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selectedRowCount:0,_originalList:[],sortConfig:{fieldId:null,direction:null},filterConfig:{},activeFieldId:null,activeField:null,activeModes:[],_columnStats:{},_internalUpdate:!1,_appliedHash:"",_fieldsDirty:!1,_masterCellNodes:null,isDirty:!1,isBulkEdit:null});const s=(()=>{let e=!!globalThis.__DT_PERF_MODE__;const t={refreshTime:0,refreshCount:0,scrollCount:0,totalNodes:0};return e&&!globalThis.__statePerformanceTelemetry&&(globalThis.__statePerformanceTelemetry={scanCount:0,reuseCount:0,moveCount:0}),{get stats(){return t},enable:()=>{e=!0},disable:()=>{e=!1},onScroll:()=>{e&&t.scrollCount++},startFrame:()=>{var t,i,n;return e?{start:performance.now(),scan:(null==(t=globalThis.__statePerformanceTelemetry)?void 0:t.scanCount)||0,move:(null==(i=globalThis.__statePerformanceTelemetry)?void 0:i.moveCount)||0,reuse:(null==(n=globalThis.__statePerformanceTelemetry)?void 0:n.reuseCount)||0}:null},endFrame:(i,n)=>{if(!e||!i)return;t.refreshCount++,t.totalNodes+=n;const l=performance.now()-i.start;t.refreshTime+=l;const s=globalThis.__statePerformanceTelemetry;if(s){const e=s.scanCount-i.scan,t=s.moveCount-i.move,o=s.reuseCount-i.reuse;(e>0||l>2)&&console.log(`[DataTable Frame] Time: ${l.toFixed(2)}ms, Scans: ${e}, Moves: ${t}, Reuses: ${o}, Rows: ${n}`)}}}})();l.perf=s.stats;const o=((e,t)=>{let i=null,n=null,l=[];const s=(e,t)=>!!(i&&e>=i.minRow&&e<=i.maxRow&&t>=i.minCol&&t<=i.maxCol)||l.some(i=>e>=i.minRow&&e<=i.maxRow&&t>=i.minCol&&t<=i.maxCol);let o=!1;const a=()=>{if(globalThis.__DT_FEATURES__&&!globalThis.__DT_FEATURES__.selection)return;let n=1/0,a=-1/0;i&&(n=Math.min(n,i.minRow),a=Math.max(a,i.maxRow)),l.forEach(e=>{n=Math.min(n,e.minRow),a=Math.max(a,e.maxRow)});const r=n!==1/0;if(!r&&!o)return;o=r;const d=e.querySelector(".dt-body");d&&d.querySelectorAll(".dt-body-row").forEach(e=>{var i;const l=((null==(i=e._ref)?void 0:i.rIdx)??-1)+t._listStartIndex,o=e.querySelectorAll(".dt-cell");!r||la?o.forEach(e=>e.classList.remove("dt-cell-selected")):o.forEach((e,t)=>{s(l,t)?e.classList.add("dt-cell-selected"):e.classList.remove("dt-cell-selected")})})},r=()=>{let e=0;i&&(e+=i.maxRow-i.minRow+1),l.forEach(t=>e+=t.maxRow-t.minRow+1),t.selectedRowCount=e},d=(e=!1)=>{e||(i=null,n=null,l=[],a(),r())},c=()=>{if(!i)return null;let e=i.minRow,t=i.maxRow,n=i.minCol,s=i.maxCol;return l.forEach(i=>{e=Math.min(e,i.minRow),t=Math.max(t,i.maxRow),n=Math.min(n,i.minCol),s=Math.max(s,i.maxCol)}),{minRow:e,maxRow:t,minCol:n,maxCol:s}};return{applySelectionUI:a,clearAllActive:d,startSelect:(o,c,u)=>{const m=s(o,c),p=i&&(i.minRow!==i.maxRow||i.minCol!==i.maxCol)||l.length>0;u.shiftKey&&n?i={minRow:Math.min(n.row,o),maxRow:Math.max(n.row,o),minCol:Math.min(n.col,c),maxCol:Math.max(n.col,c)}:(!m||u.ctrlKey||u.metaKey?(u.ctrlKey||u.metaKey?i&&!m&&l.push(i):d(),n={row:o,col:c},i={minRow:o,maxRow:o,minCol:c,maxCol:c}):p||(e._potentialCancel={row:o,col:c}),t.isSelecting=!0),a(),r(),e.focus()},updateSelect:(l,s)=>{t.isSelecting&&n&&(i={minRow:Math.min(n.row,l),maxRow:Math.max(n.row,l),minCol:Math.min(n.col,s),maxCol:Math.max(n.col,s)},e._potentialCancel=null,a(),r())},endSelect:()=>{if(e._potentialCancel){const{row:t,col:i}=e._potentialCancel;s(t,i)&&d(),e._potentialCancel=null}t.isSelecting=!1},getSelectionBounds:c,copy:async()=>{const e=c();if(!e)return;const i=t.list.slice(e.minRow,e.maxRow+1).map(i=>t.fields.slice(e.minCol,e.maxCol+1).map(e=>{let t=String(i[e.id]??"");return(t.includes("\t")||t.includes("\n")||t.includes('"'))&&(t='"'+t.replace(/"/g,'""')+'"'),t}).join("\t")).join("\n");await navigator.clipboard.writeText(i)},paste:async()=>{try{const i=await navigator.clipboard.readText();if(!i)return;const n=c();if(!n)return;const l=i.split(/\r?\n/).filter(e=>e.length>0).map(e=>{const t=[];let i="",n=!1;for(let l=0;l{var t;return null==(t=e.classList)?void 0:t.contains("dt-body-row")});let u=!1;l.forEach((e,i)=>{const n=s+i;if(n>a||n>=t.list.length)return;const l=t.list[n];let d=!1;e.forEach((e,i)=>{const n=o+i;if(n>r||n>=t.fields.length)return;const s=t.fields[n];l[s.id]=e,d=!0}),d&&(u=!0)}),u&&(t.list=[...t.list])}catch(e){console.error("Paste Error:",e)}}}})(n,l),a=((e,t,i)=>{const n=globalThis.VirtualScroll({itemHeight:40});let l=null;const s=(e=!1)=>{if(!l)return;const s=n.calc(l,t.list);if(s){if(!e&&t.prevHeight===s.prevHeight&&t.postHeight===s.postHeight&&t._listStartIndex===s.listStartIndex&&t._renderedList.length===s.renderedList.length)return;Object.assign(t,{prevHeight:s.prevHeight,postHeight:s.postHeight,_listStartIndex:s.listStartIndex,_renderedList:s.renderedList}),null==i||i(s.renderedList.length,e)}};return{init:()=>{l=e.querySelector(".dt-main")},reset:i=>{t._listStartIndex=0,n.reset(i,l||e),t.list===i&&n.init(i,()=>s(!0))},refresh:s,onScroll:()=>s(!1)}})(n,l,()=>o.applySelectionUI()),r=n.querySelector(".dt-column-menu");r&&(r._thisObj=n),n.onColumnResizing=(e,t)=>n.style.setProperty(`--w-${e.id}`,t.detail.newSize+"px"),n.onColumnResize=(e,t)=>{const i=l.fields.findIndex(t=>t.id===e.id);-1!==i&&(l.fields[i].width=t.detail.newSize,l.fields=[...l.fields])};let d,c=null;n.format=(e,t)=>{var n;if(t.formatter)return t.formatter(e,t);const l=i._fieldTypes.get((null==(n=t.settings)?void 0:n.formType)||t.type||"text");return l&&l.formatter?l.formatter(e,t):null==e?"":"object"==typeof e?JSON.stringify(e):String(e)},n.onScroll=()=>{s.onScroll(),a.refresh(),n.hideColumnMenu();const e=n.querySelector(".dt-spacer-prev"),t=n.querySelector(".dt-spacer-post");e&&(e.style.height=(l.prevHeight||0)+"px",e.style.display=l.prevHeight>0?"block":"none"),t&&(t.style.height=(l.postHeight||0)+"px",t.style.display=l.postHeight>0?"block":"none")},n.applySortFilter=(e={})=>{if(l._internalUpdate)return;const t={...l.filterConfig,...e.filters||{}},i=void 0!==e.sort?e.sort?{fieldId:l.activeFieldId,direction:e.sort}:{fieldId:null,direction:null}:l.sortConfig;let n=[...l._originalList];if(Object.entries(t).forEach(([e,t])=>{(t.value||t.selectedValues&&0!==t.selectedValues.length)&&(n=n.filter(i=>{var n;const l=i[e];if((null==(n=t.selectedValues)?void 0:n.length)>0)return t.selectedValues.includes(String(l));const s=String(t.value).toLowerCase(),o=String(l??"").toLowerCase();switch(t.mode){case"contains":return o.includes(s);case"equals":return o===s;case"starts":return o.startsWith(s);case"ends":return o.endsWith(s);case"=":return Number(l)===Number(t.value);case">":return Number(l)>Number(t.value);case"<":return Number(l)=Number(t.value)&&Number(l)<=Number(t.value2);default:return!0}}))}),i.fieldId&&i.direction){const e=i.fieldId,t="asc"===i.direction?1:-1;n.sort((i,n)=>i[e]==n[e]?0:i[e]>n[e]?t:-t)}l._internalUpdate=!0,l.filterConfig=t,l.sortConfig=i,l.list=n,l._internalUpdate=!1},n.showColumnMenu=(t,i)=>{var s;const o=i.currentTarget,a=n.querySelector(".dt-column-menu"),r=(null==(s=t.settings)?void 0:s.formType)||t.type||"text";l.activeModes=e[r]||(["boolean","switch","checkbox","radio"].includes(r)?[]:e.text),l.filterConfig[t.id]||(l.filterConfig[t.id]={mode:l.activeModes[0]||"contains",value:"",selectedValues:[]}),l.activeField=t,l.activeFieldId=t.id,a.style.display="block";const d=o.closest(".dt-cell").getBoundingClientRect(),c=n.getBoundingClientRect(),u=a.offsetWidth||260;let m=d.right-c.left-u;m<0&&(m=Math.max(0,d.left-c.left)),a.style.left=m+"px",a.style.top=d.bottom-c.top+5+"px";const p=e=>{a.contains(e.target)||o.contains(e.target)||(n.hideColumnMenu(),n.applySortFilter(),document.removeEventListener("mousedown",p))};document.addEventListener("mousedown",p),setTimeout(()=>{var e;return null==(e=a.querySelector("input"))?void 0:e.focus()},50)},n.toggleSelectedValue=e=>{const t=l.filterConfig[l.activeFieldId];if(!t)return;const i=t.selectedValues.indexOf(e);-1===i?t.selectedValues.push(e):t.selectedValues.splice(i,1),l.filterConfig={...l.filterConfig},n.applySortFilter()},n.filterOnlyThis=e=>{l.filterConfig[l.activeFieldId]={mode:"contains",value:"",selectedValues:[String(e)]},l.filterConfig={...l.filterConfig},n.applySortFilter()},n.hideColumnMenu=()=>{const e=n.querySelector(".dt-column-menu");e&&(e.style.display="none")},n.setSort=e=>{const t=l.sortConfig.direction===e&&l.sortConfig.fieldId===l.activeFieldId?null:e;n.applySortFilter({sort:t})},n.clearColumnSettings=()=>{l.activeFieldId&&(delete l.filterConfig[l.activeFieldId],l.filterConfig={...l.filterConfig},n.applySortFilter())},n._initRow=e=>{var t;const i=null==(t=e._ref)?void 0:t.item;i&&void 0===i._editingF&&Object.defineProperty(i,"_editingF",{set:e=>{null===e&&n.hideEditor(!0)},configurable:!0}),Array.from(e.children).forEach(t=>{const i=parseInt(t.dataset.fidx);isNaN(i)||(t._ref={...t._ref||e._ref,f:l.fields[i],fIdx:i})})},l.__watch("fields",e=>{if(!e)return;l._fieldsDirty=!0,l._masterCellNodes=null,n.style.setProperty("--dt-grid-template",e.map(e=>{var t;return`var(--w-${e.id}, ${(null==(t=e.settings)?void 0:t.width)||e.width||150}px)`}).join(" ")),n.style.setProperty("--dt-row-width",e.reduce((e,t)=>{var i;return e+((null==(i=t.settings)?void 0:i.width)||t.width||150)},0)+"px");let t=0;e.forEach(e=>{var i,l;"left"===((null==(i=e.settings)?void 0:i.pinned)||e.pinned)&&(n.style.setProperty(`--l-${e.id}`,t+"px"),t+=(null==(l=e.settings)?void 0:l.width)||e.width||150)});let i=0;[...e].reverse().forEach(e=>{var t,l;"right"===((null==(t=e.settings)?void 0:t.pinned)||e.pinned)&&(n.style.setProperty(`--r-${e.id}`,i+"px"),i+=(null==(l=e.settings)?void 0:l.width)||e.width||150)})}),l.__watch("list",e=>{var t;if(l._fieldsDirty){l._fieldsDirty=!1;const e=null==(t=n.querySelector('.dt-body template[index="rIdx"]'))?void 0:t.content.querySelector('template[as="f"]');if(e){const t=l._masterCellNodes||(l._masterCellNodes=Array.from(e.content.childNodes).map(e=>e.cloneNode(!0)));e.removeAttribute("$each"),e.setAttribute("$if","true"),e.content.textContent="",l.fields.forEach((i,n)=>t.forEach(t=>{var l;const s=t.cloneNode(!0);if(1===s.nodeType){s.dataset.fidx=n;const e=(null==(l=i.settings)?void 0:l.pinned)||i.pinned;e&&(s.classList.add("pinned-"+e),s.style.position="sticky",s.style.zIndex="1",s.style.backgroundColor="inherit","left"===e?(s.style.left=`var(--l-${i.id})`,s.style.borderRight="1px solid var(--bs-border-color)",s.style.boxShadow="2px 0 5px -2px rgba(0,0,0,0.1)"):(s.style.right=`var(--r-${i.id})`,s.style.borderLeft="1px solid var(--bs-border-color)",s.style.boxShadow="-2px 0 5px -2px rgba(0,0,0,0.1)"))}e.content.appendChild(s)}))}}l._internalUpdate||(l._originalList=[...e||[]],setTimeout(()=>{const e={};l.fields.forEach(t=>{const i={};l._originalList.forEach(e=>{const n=e[t.id],l=null==n||""===n?"":String(n);i[l]=(i[l]||0)+1}),e[t.id]=Object.entries(i).sort((e,t)=>t[1]-e[1]).slice(0,20).map(([e,t])=>({val:e,count:t}))}),l._columnStats=e},200)),a.init(),a.reset(e)}),n.editCell=(e,t,i)=>{var l,s;const o=n.querySelector(".dt-editor-overlay"),a=i.getBoundingClientRect(),r=n.getBoundingClientRect();c=i;const d=(null==(l=t.settings)?void 0:l.formType)||t.type||"text",u=o.querySelector("AutoForm");u&&(u.data=e,u.state.schema=[{...t,type:d,options:(null==(s=t.settings)?void 0:s.options)||t.options,name:t.id,label:""}]),Object.assign(o.style,{display:"flex",left:a.left-r.left+"px",top:a.top-r.top+"px",width:("textarea"===d||"TagsInput"===d?Math.max(a.width,300):a.width)+"px",height:"textarea"===d||"TagsInput"===d?"auto":a.height+"px"}),setTimeout(()=>{var e;return null==(e=o.querySelector("input, textarea, select, .form-control"))?void 0:e.focus()},30)},n.hideEditor=(e=!0)=>{if(d||(d=n.querySelector(".dt-editor-overlay")),!d||"none"===d.style.display)return;const t=d.querySelector("AutoForm");if(e&&t&&t.data){const e=d.querySelector("input:focus, select:focus, textarea:focus");e&&e.dispatchEvent(new Event("number"===e.type||"SELECT"===e.tagName?"change":"input",{bubbles:!0}));if((t.state.schema||[]).forEach(e=>{var i,n;const l=null==(n=null==(i=null==c?void 0:c.closest(".dt-row"))?void 0:i._ref)?void 0:n.item;l&&(l[e.name]=t.data[e.name])}),l.isBulkEdit){const{minRow:e,maxRow:i,fIdx:n}=l.isBulkEdit,s=l.fields[n],o=t.data[s.id];for(let t=e;t<=i;t++)l.list[t]&&(l.list[t][s.id]=o)}l.list=[...l.list],l.isDirty=!0}d.style.display="none",t&&(t.state.schema=[],t.data=null),c=null,l.isBulkEdit=null,n.focus()},n.onMainMouseDown=e=>{var t;const i=e.target.closest(".dt-cell"),s=null==i?void 0:i.closest(".dt-row");if(!s||s.classList.contains("dt-header-row"))return;const a=i.dataset.fidx?parseInt(i.dataset.fidx):Array.from(s.querySelectorAll(".dt-cell")).indexOf(i),r=(null==(t=s._ref)?void 0:t.rIdx)??Array.from(n.querySelectorAll(".dt-body-row")).indexOf(s);o.startSelect(r+l._listStartIndex,a,e)},n.onMainMouseOver=e=>{var t;if(l.isSelecting){const i=e.target.closest(".dt-cell"),s=null==i?void 0:i.closest(".dt-row");if(s&&!s.classList.contains("dt-header-row")){const e=i.dataset.fidx?parseInt(i.dataset.fidx):Array.from(s.querySelectorAll(".dt-cell")).indexOf(i),a=(null==(t=s._ref)?void 0:t.rIdx)??Array.from(n.querySelectorAll(".dt-body-row")).indexOf(s);o.updateSelect(a+l._listStartIndex,e)}}},n.onMainDblClick=e=>{var t,i,s;const a=e.target.closest(".dt-cell"),r=null==a?void 0:a.closest(".dt-row");if(r&&!r.classList.contains("dt-header-row")){const e=null==(t=r._ref)?void 0:t.item,d=a.dataset.fidx?parseInt(a.dataset.fidx):Array.from(r.querySelectorAll(".dt-cell")).indexOf(a),c=((null==(i=r._ref)?void 0:i.rIdx)??Array.from(n.querySelectorAll(".dt-body-row")).indexOf(r))+l._listStartIndex;if(e&&l.fields[d]){const t=o.getSelectionBounds();if(t&&c>=t.minRow&&c<=t.maxRow&&d>=t.minCol&&d<=t.maxCol){const e=t.maxRow-t.minRow+1;e>1&&(l.isBulkEdit={...t,fIdx:d},(null==(s=globalThis.UI)?void 0:s.toast)&&globalThis.UI.toast(`Bulk Edit: Updating ${e} rows in column "${l.fields[d].name}"`,{type:"warning"}))}n.editCell(e,l.fields[d],a)}}},n.addRow=()=>{const e={};l.fields.forEach(t=>e[t.id]=""),l._originalList.push(e),l.list=[...l._originalList],l.isDirty=!0,setTimeout(()=>{a.reset(l.list),n.querySelector(".dt-main").scrollTop=n.querySelector(".dt-main").scrollHeight},50)},n.deleteSelectedRow=async()=>{const e=o.getSelectionBounds();if(!e)return;const t=e.maxRow-e.minRow+1;if(await globalThis.UI.confirm(`Are you sure you want to delete ${t} row(s)?`)){const t=e.minRow,i=e.maxRow,s=l.list.slice(t,i+1);l.list=l.list.filter((e,n)=>!(n>=t&&n<=i)),l._originalList=l._originalList.filter(e=>!s.includes(e)),l.isDirty=!0,o.clearAllActive(),n.dispatchEvent(new CustomEvent("remove",{detail:{items:s}}))}},n.saveChanges=()=>{n.dispatchEvent(new CustomEvent("save",{detail:{list:l._originalList,fields:l.fields}})),l.isDirty=!1};const u=()=>{const e=globalThis.DataTable.getFieldTypes(),t=[{name:"id",label:"Field ID",type:"text",setting:{required:!0,placeholder:"e.g. user_name"}},{name:"name",label:"Display Name",type:"text",setting:{required:!0,placeholder:"e.g. 用户名"}},{name:"user_type",label:"Field Type",type:"select",options:e.map(e=>({label:e.label,value:e.value}))}],i=e.reduce((e,t)=>e.concat(t.schema||[]),[]);return t.concat(i,[{name:"isIndex",label:"Index",type:"switch"},{name:"memo",label:"Memo",type:"text"}])},m=e=>{if(e)return e.split("\n").map(e=>e.trim()).filter(Boolean).map(e=>{const t=e.indexOf(":");return t>-1?{label:e.slice(0,t).trim(),value:e.slice(t+1).trim()}:e})};n.addField=async()=>{n.hideColumnMenu();const e=globalThis.NewState({id:"c"+Date.now().toString().slice(-4),name:"New Field",user_type:"text",decimals:0,isIndex:!1,memo:"",options_str:""}),t=n.querySelector(`Dialog[id="${n.id}_field_dialog"]`);if(!t)return;Object.assign(t.state,{title:"Add Field",buttons:["Cancel","Save"]});const i=t.querySelector("AutoForm");i&&(i.data=e,i.state.schema=u()),t.show();if(2===await new Promise(e=>t.addEventListener("change",i=>e(t.result),{once:!0}))){const t=globalThis.DataTable.getFieldTypes().find(t=>t.value===e.user_type);let i=(null==t?void 0:t.typeForDB)||"v1024";"number"===e.user_type&&(i=e.decimals>0?"ff":"bi");const s={id:e.id,name:e.name,memo:e.memo,isIndex:!!e.isIndex,type:i,settings:{formType:e.user_type,decimals:e.decimals,prefix:e.prefix,suffix:e.suffix,thousandSep:e.thousandSep,labelOn:e.labelOn,labelOff:e.labelOff,format:e.format,placeholder:e.placeholder,options:m(e.options_str)}};l.fields=[...l.fields,s],l.isDirty=!0,n.dispatchEvent(new CustomEvent("savefields",{detail:l.fields})),l.list=[...l.list]}},n.editField=async()=>{if(!l.activeField)return;n.hideColumnMenu();const e=l.activeField,t=e.settings||{},i=globalThis.NewState({id:e.id,name:e.name,memo:e.memo||"",isIndex:!!e.isIndex,user_type:t.formType||"text",decimals:t.decimals||0,prefix:t.prefix||"",suffix:t.suffix||"",thousandSep:!!t.thousandSep,labelOn:t.labelOn||"",labelOff:t.labelOff||"",format:t.format||"",placeholder:t.placeholder||"",options_str:(s=t.options,s?s.map(e=>"object"==typeof e?`${e.label}:${e.value}`:e).join("\n"):"")});var s;const o=n.querySelector(`Dialog[id="${n.id}_field_dialog"]`);if(!o)return;Object.assign(o.state,{title:"Edit Field",buttons:["Cancel","Save"]});const a=o.querySelector("AutoForm");a&&(a.data=i,a.state.schema=u()),o.show();if(2===await new Promise(e=>o.addEventListener("change",t=>e(o.result),{once:!0}))){const t=l.fields.findIndex(t=>t.id===e.id);if(-1!==t){const s=globalThis.DataTable.getFieldTypes().find(e=>e.value===i.user_type);let o=(null==s?void 0:s.typeForDB)||"v1024";"number"===i.user_type&&(o=i.decimals>0?"ff":"bi");const a={...e,id:i.id,name:i.name,memo:i.memo,isIndex:!!i.isIndex,type:o,settings:{...e.settings,formType:i.user_type,decimals:i.decimals,prefix:i.prefix,suffix:i.suffix,thousandSep:i.thousandSep,labelOn:i.labelOn,labelOff:i.labelOff,format:i.format,placeholder:i.placeholder,options:m(i.options_str)}};l.fields[t]=a,l.fields=[...l.fields],l.isDirty=!0,n.dispatchEvent(new CustomEvent("savefields",{detail:l.fields})),l.list=[...l.list]}}},n.deleteField=async()=>{if(l.activeField&&(n.hideColumnMenu(),await globalThis.UI.confirm(`Are you sure you want to delete field "${l.activeField.name}"?`))){const e=l.fields.findIndex(e=>e.id===l.activeField.id);-1!==e&&(l.fields.splice(e,1),l.fields=[...l.fields],l.isDirty=!0,n.dispatchEvent(new CustomEvent("savefields",{detail:l.fields})),l.list=[...l.list])}},window.addEventListener("mouseup",o.endSelect),document.addEventListener("mousedown",e=>{const t=n.querySelector(".dt-editor-overlay"),i=n.querySelector(".dt-column-menu");"none"===(null==t?void 0:t.style.display)||t.contains(e.target)||n.hideEditor(!0),n.contains(e.target)||(null==t?void 0:t.contains(e.target))||(null==i?void 0:i.contains(e.target))||o.clearAllActive()}),l._MODE_ICONS=t},globalThis.Util.makeDom('\n\n
\n\n
\n
\n \n \n \n
\n\n \n\n \n \n \n \n
\n \n\n \n
TOP FREQUENT VALUES
\n
\n \n \n
\n\n \n Clear Filter\n
\n\n \n \n \n \n
\n \n
\n\n
\n\n
\n\n
\n
\n'),globalThis.Util.makeDom("\n\n")),globalThis.DataTable=i});
diff --git a/package-lock.json b/package-lock.json
index 8ec27a8..dbf23b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@web/datatable",
- "version": "1.0.4",
+ "version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@web/datatable",
- "version": "1.0.4",
+ "version": "1.0.5",
"devDependencies": {
"@playwright/test": "^1.40.0",
"@rollup/plugin-terser": "^1.0.0",
diff --git a/package.json b/package.json
index 1e1d1d3..9f9657f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@web/datatable",
- "version": "1.0.4",
+ "version": "1.0.8",
"type": "module",
"main": "dist/datatable.js",
"module": "dist/datatable.js",
diff --git a/src/DataTable.js b/src/DataTable.js
new file mode 100644
index 0000000..4affdc2
--- /dev/null
+++ b/src/DataTable.js
@@ -0,0 +1,737 @@
+/**
+ * DataTable Component Module
+ * Consolidated with perf, scroll, and selection managers.
+ */
+
+// Global Configuration
+const MODE_MAP = {
+ text: ['contains', 'equals', 'starts', 'ends'],
+ textarea: ['contains', 'equals', 'starts', 'ends'],
+ number: ['=', '>', '<', 'between'],
+ date: ['=', '>', '<', 'between'],
+ select: ['contains', 'equals'],
+ TagsInput: ['contains', 'equals', 'starts', 'ends']
+};
+
+const MODE_ICONS = {
+ 'contains': 'bi-search', 'equals': 'bi-distribute-vertical', 'starts': 'bi-align-start', 'ends': 'bi-align-end',
+ '=': 'bi-calculator', '>': 'bi-chevron-right', '<': 'bi-chevron-left', 'between': 'bi-arrows-expand'
+};
+
+const DataTableConfig = {
+ _fieldTypes: new Map(),
+ registerFieldType: (config) => {
+ DataTableConfig._fieldTypes.set(config.value, config);
+ },
+ getFieldTypes: () => Array.from(DataTableConfig._fieldTypes.values())
+};
+
+// Register Built-in Types
+DataTableConfig.registerFieldType({
+ value: 'text', label: '{#Text#}', typeForDB: 'v4096',
+ schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="text"' }]
+});
+DataTableConfig.registerFieldType({
+ value: 'number', label: '{#Number#}', typeForDB: 'ff',
+ schema: [
+ { name: 'decimals', label: 'Decimals', type: 'number', setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' },
+ { name: 'prefix', label: 'Prefix (e.g. $)', type: 'text', if: 'this.data.user_type=="number"' },
+ { name: 'suffix', label: 'Suffix (e.g. %)', type: 'text', if: 'this.data.user_type=="number"' },
+ { name: 'thousandSep', label: 'Thousand Sep', type: 'switch', if: 'this.data.user_type=="number"' }
+ ],
+ formatter: (val, field) => {
+ if (val == null || val === '') return '';
+ let num = Number(val);
+ if (isNaN(num)) return val;
+ const s = field.settings || {};
+ if (s.decimals !== undefined) num = num.toFixed(s.decimals);
+ let str = String(num);
+ if (s.thousandSep) {
+ const parts = str.split('.');
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ str = parts.join('.');
+ }
+ return (s.prefix || '') + str + (s.suffix || '');
+ }
+});
+DataTableConfig.registerFieldType({
+ value: 'select', label: '{#Single Select#}', typeForDB: 'v1024',
+ schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="select"' }],
+ formatter: (val, field) => {
+ if (val == null || val === '') return '';
+ const opts = field.settings?.options || [];
+ const opt = opts.find(o => typeof o === 'object' ? o.value == val : o == val);
+ return opt ? (typeof opt === 'object' ? opt.label : opt) : val;
+ }
+});
+DataTableConfig.registerFieldType({
+ value: 'checkbox', label: '{#Multi Select#}', typeForDB: 'v4096',
+ schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="checkbox"' }],
+ formatter: (val, field) => {
+ if (!Array.isArray(val)) return val == null ? '' : String(val);
+ const opts = field.settings?.options || [];
+ return val.map(v => {
+ const opt = opts.find(o => typeof o === 'object' ? o.value == v : o == v);
+ return opt ? (typeof opt === 'object' ? opt.label : opt) : v;
+ }).join(', ');
+ }
+});
+DataTableConfig.registerFieldType({
+ value: 'switch', label: '{#Switch#}', typeForDB: 'b',
+ schema: [
+ { name: 'labelOn', label: 'Label On', type: 'text', if: 'this.data.user_type=="switch"' },
+ { name: 'labelOff', label: 'Label Off', type: 'text', if: 'this.data.user_type=="switch"' }
+ ],
+ formatter: (val, field) => {
+ const s = field.settings || {};
+ return val ? (s.labelOn || 'Yes') : (s.labelOff || 'No');
+ }
+});
+DataTableConfig.registerFieldType({
+ value: 'datetime', label: '{#DateTime#}', typeForDB: 'dt',
+ schema: [{ name: 'format', label: 'Format', type: 'text', setting: { placeholder: 'YYYY-MM-DD' }, if: 'this.data.user_type=="datetime"' }]
+});
+DataTableConfig.registerFieldType({
+ value: 'textarea', label: '{#Long Text#}', typeForDB: 't',
+ schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="textarea"' }]
+});
+
+// Managers Factory Functions (Internalized)
+const createPerfMonitor = () => {
+ let enabled = !!globalThis.__DT_PERF_MODE__;
+ const stats = { refreshTime: 0, refreshCount: 0, scrollCount: 0, totalNodes: 0 };
+ if (enabled && !globalThis.__statePerformanceTelemetry) {
+ globalThis.__statePerformanceTelemetry = { scanCount: 0, reuseCount: 0, moveCount: 0 };
+ }
+ return {
+ get stats() { return stats; },
+ enable: () => { enabled = true; },
+ disable: () => { enabled = false; },
+ onScroll: () => { if (enabled) stats.scrollCount++; },
+ startFrame: () => {
+ if (!enabled) return null;
+ return {
+ start: performance.now(),
+ scan: globalThis.__statePerformanceTelemetry?.scanCount || 0,
+ move: globalThis.__statePerformanceTelemetry?.moveCount || 0,
+ reuse: globalThis.__statePerformanceTelemetry?.reuseCount || 0
+ };
+ },
+ endFrame: (startData, renderedCount) => {
+ if (!enabled || !startData) return;
+ stats.refreshCount++; stats.totalNodes += renderedCount;
+ const elapsed = performance.now() - startData.start;
+ stats.refreshTime += elapsed;
+ const stPerf = globalThis.__statePerformanceTelemetry;
+ if (stPerf) {
+ const scans = stPerf.scanCount - startData.scan;
+ const moves = stPerf.moveCount - startData.move;
+ const reuses = stPerf.reuseCount - startData.reuse;
+ if (scans > 0 || elapsed > 2) {
+ console.log(`[DataTable Frame] Time: ${elapsed.toFixed(2)}ms, Scans: ${scans}, Moves: ${moves}, Reuses: ${reuses}, Rows: ${renderedCount}`);
+ }
+ }
+ }
+ };
+};
+
+const createScrollManager = (container, state, onRenderedListChange) => {
+ const vs = globalThis.VirtualScroll({ itemHeight: 40 });
+ let scrollEl = null;
+ const refresh = (isLayoutChange = false) => {
+ if (!scrollEl) return;
+ const res = vs.calc(scrollEl, state.list);
+ if (res) {
+ if (!isLayoutChange && state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) return;
+ Object.assign(state, { prevHeight: res.prevHeight, postHeight: res.postHeight, _listStartIndex: res.listStartIndex, _renderedList: res.renderedList });
+ onRenderedListChange?.(res.renderedList.length, isLayoutChange);
+ }
+ };
+ return {
+ init: () => { scrollEl = container.querySelector('.dt-main'); },
+ reset: (list) => { state._listStartIndex = 0; vs.reset(list, scrollEl || container); if (state.list === list) vs.init(list, () => refresh(true)); },
+ refresh, onScroll: () => refresh(false)
+ };
+};
+
+const createSelectionManager = (container, state) => {
+ let activeBounds = null; let startCell = null; let multiSelections = [];
+ const isCellSelected = (r, c) => {
+ if (activeBounds && r >= activeBounds.minRow && r <= activeBounds.maxRow && c >= activeBounds.minCol && c <= activeBounds.maxCol) return true;
+ return multiSelections.some(s => r >= s.minRow && r <= s.maxRow && c >= s.minCol && c <= s.maxCol);
+ };
+ let lastHadSelection = false;
+ const applySelectionUI = () => {
+ if (globalThis.__DT_FEATURES__ && !globalThis.__DT_FEATURES__.selection) return;
+ let boundMinRow = Infinity, boundMaxRow = -Infinity;
+ if (activeBounds) { boundMinRow = Math.min(boundMinRow, activeBounds.minRow); boundMaxRow = Math.max(boundMaxRow, activeBounds.maxRow); }
+ multiSelections.forEach(s => { boundMinRow = Math.min(boundMinRow, s.minRow); boundMaxRow = Math.max(boundMaxRow, s.maxRow); });
+ const hasSelection = boundMinRow !== Infinity;
+ if (!hasSelection && !lastHadSelection) return;
+ lastHadSelection = hasSelection;
+ const body = container.querySelector('.dt-body');
+ if (!body) return;
+ const rowNodes = body.querySelectorAll('.dt-body-row');
+ rowNodes.forEach(rowNode => {
+ const absoluteRow = (rowNode._ref?.rIdx ?? -1) + state._listStartIndex;
+ const cells = rowNode.querySelectorAll('.dt-cell');
+ if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) { cells.forEach(cell => cell.classList.remove('dt-cell-selected')); return; }
+ cells.forEach((cell, cIdx) => { if (isCellSelected(absoluteRow, cIdx)) cell.classList.add('dt-cell-selected'); else cell.classList.remove('dt-cell-selected'); });
+ });
+ };
+ const updateStatus = () => {
+ let count = 0; if (activeBounds) count += (activeBounds.maxRow - activeBounds.minRow + 1);
+ multiSelections.forEach(s => count += (s.maxRow - s.minRow + 1));
+ state.selectedRowCount = count;
+ };
+ const clearAllActive = (keepSelection = false) => { if (!keepSelection) { activeBounds = null; startCell = null; multiSelections = []; applySelectionUI(); updateStatus(); } };
+ const startSelect = (row, col, e) => {
+ const alreadySelected = isCellSelected(row, col);
+ const isRange = (activeBounds && (activeBounds.minRow !== activeBounds.maxRow || activeBounds.minCol !== activeBounds.maxCol)) || multiSelections.length > 0;
+ if (e.shiftKey && startCell) { activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) }; }
+ else {
+ if (alreadySelected && !e.ctrlKey && !e.metaKey) { if (!isRange) container._potentialCancel = { row, col }; }
+ else { if (!e.ctrlKey && !e.metaKey) clearAllActive(); else if (activeBounds && !alreadySelected) multiSelections.push(activeBounds); startCell = { row, col }; activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col }; }
+ state.isSelecting = true;
+ }
+ applySelectionUI(); updateStatus(); container.focus();
+ };
+ const updateSelect = (row, col) => { if (state.isSelecting && startCell) { activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) }; container._potentialCancel = null; applySelectionUI(); updateStatus(); } };
+ const endSelect = () => { if (container._potentialCancel) { const { row, col } = container._potentialCancel; if (isCellSelected(row, col)) clearAllActive(); container._potentialCancel = null; } state.isSelecting = false; };
+ const getSelectionBounds = () => {
+ if (!activeBounds) return null;
+ let minRow = activeBounds.minRow, maxRow = activeBounds.maxRow, minCol = activeBounds.minCol, maxCol = activeBounds.maxCol;
+ multiSelections.forEach(s => { minRow = Math.min(minRow, s.minRow); maxRow = Math.max(maxRow, s.maxRow); minCol = Math.min(minCol, s.minCol); maxCol = Math.max(maxCol, s.maxCol); });
+ return { minRow, maxRow, minCol, maxCol };
+ };
+ const copy = async () => {
+ const bounds = getSelectionBounds(); if (!bounds) return;
+ const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map(row => {
+ return state.fields.slice(bounds.minCol, bounds.maxCol + 1).map(f => {
+ let val = String(row[f.id] ?? '');
+ if (val.includes('\t') || val.includes('\n') || val.includes('"')) val = '"' + val.replace(/"/g, '""') + '"';
+ return val;
+ }).join('\t');
+ }).join('\n');
+ await navigator.clipboard.writeText(text);
+ };
+ const paste = async () => {
+ try {
+ const text = await navigator.clipboard.readText(); if (!text) return;
+ const bounds = getSelectionBounds(); if (!bounds) return;
+ const rows = text.split(/\r?\n/).filter(line => line.length > 0).map(line => {
+ const cells = []; let current = '', inQuotes = false;
+ for (let i = 0; i < line.length; i++) {
+ const char = line[i];
+ if (char === '"') { if (inQuotes && line[i+1] === '"') { current += '"'; i++; } else inQuotes = !inQuotes; }
+ else if (char === '\t' && !inQuotes) { cells.push(current); current = ''; }
+ else current += char;
+ }
+ cells.push(current); return cells;
+ });
+ const { minRow: startRow, minCol: startCol, maxRow, maxCol } = bounds;
+ const body = container.querySelector('.dt-body');
+ const rowNodes = body ? Array.from(body.childNodes).filter(n => n.classList?.contains('dt-body-row')) : [];
+ let anyRowChanged = false;
+ rows.forEach((rowData, rOffset) => {
+ const rIdx = startRow + rOffset; if (rIdx > maxRow || rIdx >= state.list.length) return;
+ const rowItem = state.list[rIdx]; let rowChanged = false;
+ rowData.forEach((cellData, cOffset) => { const cIdx = startCol + cOffset; if (cIdx > maxCol || cIdx >= state.fields.length) return; const field = state.fields[cIdx]; rowItem[field.id] = cellData; rowChanged = true; });
+ if (rowChanged) anyRowChanged = true;
+ });
+ if (anyRowChanged) state.list = [...state.list];
+ } catch (err) { console.error('Paste Error:', err); }
+ };
+ return { applySelectionUI, clearAllActive, startSelect, updateSelect, endSelect, getSelectionBounds, copy, paste };
+};
+
+// Component Registration
+globalThis.Component.register('DataTable', container => {
+ if (!container.state) container.state = globalThis.NewState({})
+ const state = container.state
+
+ Object.assign(state, {
+ list: [], fields: [], _renderedList: [],
+ prevHeight: 0, postHeight: 0, _listStartIndex: 0,
+ selectedRowCount: 0,
+ _originalList: [],
+ sortConfig: { fieldId: null, direction: null },
+ filterConfig: {},
+ activeFieldId: null, activeField: null, activeModes: [],
+ _columnStats: {}, _internalUpdate: false, _appliedHash: '', _fieldsDirty: false, _masterCellNodes: null,
+ isDirty: false, isBulkEdit: null
+ })
+
+ const perf = createPerfMonitor();
+ state.perf = perf.stats;
+ const selection = createSelectionManager(container, state);
+ const scroll = createScrollManager(container, state, () => selection.applySelectionUI());
+
+ const menuNode = container.querySelector('.dt-column-menu');
+ if (menuNode) menuNode._thisObj = container;
+
+ container.onColumnResizing = (field, e) => container.style.setProperty(`--w-${field.id}`, e.detail.newSize + 'px');
+ container.onColumnResize = (field, e) => {
+ const idx = state.fields.findIndex(f => f.id === field.id);
+ if (idx !== -1) { state.fields[idx].width = e.detail.newSize; state.fields = [...state.fields]; }
+ };
+
+ let _editorOverlay, currentEditingNode = null;
+
+ container.format = (val, field) => {
+ if (field.formatter) return field.formatter(val, field);
+ const typeInfo = DataTableConfig._fieldTypes.get(field.settings?.formType || field.type || 'text');
+ if (typeInfo && typeInfo.formatter) return typeInfo.formatter(val, field);
+ return val == null ? '' : (typeof val === 'object' ? JSON.stringify(val) : String(val));
+ };
+
+ container.onScroll = () => {
+ perf.onScroll(); scroll.refresh();
+ container.hideColumnMenu();
+ const prev = container.querySelector('.dt-spacer-prev'), post = container.querySelector('.dt-spacer-post');
+ if (prev) { prev.style.height = (state.prevHeight || 0) + 'px'; prev.style.display = state.prevHeight > 0 ? 'block' : 'none'; }
+ if (post) { post.style.height = (state.postHeight || 0) + 'px'; post.style.display = state.postHeight > 0 ? 'block' : 'none'; }
+ };
+
+ container.applySortFilter = (options = {}) => {
+ if (state._internalUpdate) return;
+ const targetFilters = { ...state.filterConfig, ...(options.filters || {}) };
+ const targetSort = options.sort !== undefined ? (options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null }) : state.sortConfig;
+
+ let filtered = [...state._originalList];
+ Object.entries(targetFilters).forEach(([fId, cfg]) => {
+ if (!cfg.value && (!cfg.selectedValues || cfg.selectedValues.length === 0)) return;
+ filtered = filtered.filter(item => {
+ const val = item[fId];
+ if (cfg.selectedValues?.length > 0) return cfg.selectedValues.includes(String(val));
+ const search = String(cfg.value).toLowerCase();
+ const target = String(val ?? '').toLowerCase();
+ switch (cfg.mode) {
+ case 'contains': return target.includes(search);
+ case 'equals': return target === search;
+ case 'starts': return target.startsWith(search);
+ case 'ends': return target.endsWith(search);
+ case '=': return Number(val) === Number(cfg.value);
+ case '>': return Number(val) > Number(cfg.value);
+ case '<': return Number(val) < Number(cfg.value);
+ case 'between': return Number(val) >= Number(cfg.value) && Number(val) <= Number(cfg.value2);
+ default: return true;
+ }
+ });
+ });
+
+ if (targetSort.fieldId && targetSort.direction) {
+ const fId = targetSort.fieldId;
+ const dir = targetSort.direction === 'asc' ? 1 : -1;
+ filtered.sort((a, b) => {
+ if (a[fId] == b[fId]) return 0;
+ return a[fId] > b[fId] ? dir : -dir;
+ });
+ }
+
+ state._internalUpdate = true;
+ state.filterConfig = targetFilters;
+ state.sortConfig = targetSort;
+ state.list = filtered;
+ state._internalUpdate = false;
+ };
+
+ container.showColumnMenu = (field, event) => {
+ const btn = event.currentTarget, menu = container.querySelector('.dt-column-menu');
+ const type = field.settings?.formType || field.type || 'text';
+ state.activeModes = MODE_MAP[type] || (['boolean', 'switch', 'checkbox', 'radio'].includes(type) ? [] : MODE_MAP.text);
+ if (!state.filterConfig[field.id]) state.filterConfig[field.id] = { mode: state.activeModes[0] || 'contains', value: '', selectedValues: [] };
+ state.activeField = field; state.activeFieldId = field.id;
+ menu.style.display = 'block';
+ const cellNode = btn.closest('.dt-cell'), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
+ const menuWidth = menu.offsetWidth || 260;
+ let leftPos = rect.right - rootRect.left - menuWidth;
+ if (leftPos < 0) leftPos = Math.max(0, rect.left - rootRect.left);
+ menu.style.left = leftPos + 'px'; menu.style.top = (rect.bottom - rootRect.top + 5) + 'px';
+
+ const onGlobalClick = (ev) => { if (menu.contains(ev.target) || btn.contains(ev.target)) return; container.hideColumnMenu(); container.applySortFilter(); document.removeEventListener('mousedown', onGlobalClick); };
+ document.addEventListener('mousedown', onGlobalClick);
+ setTimeout(() => menu.querySelector('input')?.focus(), 50);
+ };
+
+ container.toggleSelectedValue = (val) => {
+ const filter = state.filterConfig[state.activeFieldId]; if (!filter) return;
+ const idx = filter.selectedValues.indexOf(val); if (idx === -1) filter.selectedValues.push(val); else filter.selectedValues.splice(idx, 1);
+ state.filterConfig = { ...state.filterConfig }; container.applySortFilter();
+ };
+
+ container.filterOnlyThis = (val) => { state.filterConfig[state.activeFieldId] = { mode: 'contains', value: '', selectedValues: [String(val)] }; state.filterConfig = { ...state.filterConfig }; container.applySortFilter(); };
+ container.hideColumnMenu = () => { const menu = container.querySelector('.dt-column-menu'); if (menu) menu.style.display = 'none'; };
+ container.setSort = (dir) => { const newDir = state.sortConfig.direction === dir && state.sortConfig.fieldId === state.activeFieldId ? null : dir; container.applySortFilter({ sort: newDir }); };
+ container.clearColumnSettings = () => { if (state.activeFieldId) { delete state.filterConfig[state.activeFieldId]; state.filterConfig = { ...state.filterConfig }; container.applySortFilter(); } };
+
+ container._initRow = (rowNode) => {
+ const row = rowNode._ref?.item;
+ if (row && row._editingF === undefined) { Object.defineProperty(row, '_editingF', { set: (v) => { if (v === null) container.hideEditor(true); }, configurable: true }); }
+ Array.from(rowNode.children).forEach(cell => { const fIdx = parseInt(cell.dataset.fidx); if (!isNaN(fIdx)) cell._ref = { ...(cell._ref || rowNode._ref), f: state.fields[fIdx], fIdx: fIdx }; });
+ };
+
+ state.__watch('fields', fields => {
+ if (!fields) return;
+ state._fieldsDirty = true; state._masterCellNodes = null;
+ container.style.setProperty('--dt-grid-template', fields.map(f => `var(--w-${f.id}, ${(f.settings?.width || f.width) || 150}px)`).join(' '));
+ container.style.setProperty('--dt-row-width', fields.reduce((sum, f) => sum + ((f.settings?.width || f.width) || 150), 0) + 'px');
+
+ let leftSum = 0;
+ fields.forEach(f => {
+ const pinned = f.settings?.pinned || f.pinned;
+ if (pinned === 'left') { container.style.setProperty(`--l-${f.id}`, leftSum + 'px'); leftSum += ((f.settings?.width || f.width) || 150); }
+ });
+ let rightSum = 0;
+ [...fields].reverse().forEach(f => {
+ const pinned = f.settings?.pinned || f.pinned;
+ if (pinned === 'right') { container.style.setProperty(`--r-${f.id}`, rightSum + 'px'); rightSum += ((f.settings?.width || f.width) || 150); }
+ });
+ });
+
+ state.__watch('list', list => {
+ if (state._fieldsDirty) {
+ state._fieldsDirty = false;
+ const fieldTemplate = container.querySelector('.dt-body template[index="rIdx"]')?.content.querySelector('template[as="f"]');
+ if (fieldTemplate) {
+ const masters = state._masterCellNodes || (state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map(n => n.cloneNode(true)));
+ fieldTemplate.removeAttribute('$each'); fieldTemplate.setAttribute('$if', 'true'); fieldTemplate.content.textContent = '';
+ state.fields.forEach((f, fIdx) => masters.forEach(master => {
+ const clone = master.cloneNode(true);
+ if (clone.nodeType === 1) {
+ clone.dataset.fidx = fIdx;
+ const pinned = f.settings?.pinned || f.pinned;
+ if (pinned) {
+ clone.classList.add('pinned-' + pinned); clone.style.position = 'sticky'; clone.style.zIndex = '1'; clone.style.backgroundColor = 'inherit';
+ if (pinned === 'left') { clone.style.left = `var(--l-${f.id})`; clone.style.borderRight = '1px solid var(--bs-border-color)'; clone.style.boxShadow = '2px 0 5px -2px rgba(0,0,0,0.1)'; }
+ else { clone.style.right = `var(--r-${f.id})`; clone.style.borderLeft = '1px solid var(--bs-border-color)'; clone.style.boxShadow = '-2px 0 5px -2px rgba(0,0,0,0.1)'; }
+ }
+ }
+ fieldTemplate.content.appendChild(clone);
+ }));
+ }
+ }
+ if (!state._internalUpdate) {
+ state._originalList = [...(list || [])];
+ setTimeout(() => {
+ const stats = {};
+ state.fields.forEach(f => {
+ const counts = {};
+ state._originalList.forEach(item => { const val = item[f.id], key = (val == null || val === '') ? '' : String(val); counts[key] = (counts[key] || 0) + 1; });
+ stats[f.id] = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([val, count]) => ({ val, count }));
+ });
+ state._columnStats = stats;
+ }, 200);
+ }
+ scroll.init(); scroll.reset(list);
+ });
+
+ container.editCell = (row, field, cellNode) => {
+ const overlay = container.querySelector('.dt-editor-overlay'), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
+ currentEditingNode = cellNode;
+ const formType = field.settings?.formType || field.type || 'text';
+ const form = overlay.querySelector('AutoForm');
+ if (form) {
+ form.data = row;
+ form.state.schema = [{ ...field, type: formType, options: field.settings?.options || field.options, name: field.id, label: '' }];
+ }
+ Object.assign(overlay.style, {
+ display: 'flex', left: (rect.left - rootRect.left) + 'px', top: (rect.top - rootRect.top) + 'px',
+ width: (formType === 'textarea' || formType === 'TagsInput' ? Math.max(rect.width, 300) : rect.width) + 'px',
+ height: (formType === 'textarea' || formType === 'TagsInput' ? 'auto' : rect.height + 'px')
+ });
+ setTimeout(() => overlay.querySelector('input, textarea, select, .form-control')?.focus(), 30);
+ };
+
+ container.hideEditor = (save = true) => {
+ if (!_editorOverlay) _editorOverlay = container.querySelector('.dt-editor-overlay');
+ if (!_editorOverlay || _editorOverlay.style.display === 'none') return;
+ const form = _editorOverlay.querySelector('AutoForm');
+ if (save && form && form.data) {
+ const input = _editorOverlay.querySelector('input:focus, select:focus, textarea:focus');
+ if (input) input.dispatchEvent(new Event(input.type === 'number' || input.tagName === 'SELECT' ? 'change' : 'input', { bubbles: true }));
+
+ const schema = form.state.schema || [];
+ schema.forEach(field => {
+ const row = currentEditingNode?.closest('.dt-row')?._ref?.item;
+ if (row) row[field.name] = form.data[field.name];
+ });
+
+ if (state.isBulkEdit) {
+ const { minRow, maxRow, fIdx } = state.isBulkEdit;
+ const field = state.fields[fIdx]; const newValue = form.data[field.id];
+ for (let i = minRow; i <= maxRow; i++) { if (state.list[i]) state.list[i][field.id] = newValue; }
+ }
+ state.list = [...state.list];
+ state.isDirty = true;
+ }
+ _editorOverlay.style.display = 'none';
+ if (form) { form.state.schema = []; form.data = null; }
+ currentEditingNode = null; state.isBulkEdit = null; container.focus();
+ };
+
+ container.onMainMouseDown = e => {
+ const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row');
+ if (!row || row.classList.contains('dt-header-row')) return;
+ const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell);
+ const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
+ selection.startSelect(rIdx + state._listStartIndex, fIdx, e);
+ };
+
+ container.onMainMouseOver = e => {
+ if (state.isSelecting) {
+ const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row');
+ if (row && !row.classList.contains('dt-header-row')) {
+ const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell);
+ const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
+ selection.updateSelect(rIdx + state._listStartIndex, fIdx);
+ }
+ }
+ };
+
+ container.onMainDblClick = e => {
+ const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row');
+ if (row && !row.classList.contains('dt-header-row')) {
+ const item = row._ref?.item, fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell);
+ const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
+ const absoluteRow = rIdx + state._listStartIndex;
+
+ if (item && state.fields[fIdx]) {
+ const bounds = selection.getSelectionBounds();
+ if (bounds && absoluteRow >= bounds.minRow && absoluteRow <= bounds.maxRow && fIdx >= bounds.minCol && fIdx <= bounds.maxCol) {
+ const affectedRows = bounds.maxRow - bounds.minRow + 1;
+ if (affectedRows > 1) {
+ state.isBulkEdit = { ...bounds, fIdx };
+ if (globalThis.UI?.toast) globalThis.UI.toast(`Bulk Edit: Updating ${affectedRows} rows in column "${state.fields[fIdx].name}"`, { type: 'warning' });
+ }
+ }
+ container.editCell(item, state.fields[fIdx], cell);
+ }
+ }
+ };
+
+ container.addRow = () => {
+ const newRow = {}; state.fields.forEach(f => newRow[f.id] = '');
+ state._originalList.push(newRow); state.list = [...state._originalList]; state.isDirty = true;
+ setTimeout(() => { scroll.reset(state.list); container.querySelector('.dt-main').scrollTop = container.querySelector('.dt-main').scrollHeight; }, 50);
+ };
+
+ container.deleteSelectedRow = async () => {
+ const bounds = selection.getSelectionBounds(); if (!bounds) return;
+ const count = bounds.maxRow - bounds.minRow + 1;
+ if (await globalThis.UI.confirm(`Are you sure you want to delete ${count} row(s)?`)) {
+ const rMin = bounds.minRow, rMax = bounds.maxRow;
+ const removedItems = state.list.slice(rMin, rMax + 1);
+ state.list = state.list.filter((_, i) => !(i >= rMin && i <= rMax));
+ state._originalList = state._originalList.filter(item => !removedItems.includes(item));
+ state.isDirty = true; selection.clearAllActive();
+ container.dispatchEvent(new CustomEvent('remove', { detail: { items: removedItems } }));
+ }
+ };
+
+ container.saveChanges = () => { container.dispatchEvent(new CustomEvent('save', { detail: { list: state._originalList, fields: state.fields } })); state.isDirty = false; };
+
+ const getFieldSchema = () => {
+ const types = globalThis.DataTable.getFieldTypes();
+ const baseSchema = [
+ { name: 'id', label: 'Field ID', type: 'text', setting: { required: true, placeholder: 'e.g. user_name' } },
+ { name: 'name', label: 'Display Name', type: 'text', setting: { required: true, placeholder: 'e.g. 用户名' } },
+ { name: 'user_type', label: 'Field Type', type: 'select', options: types.map(t => ({ label: t.label, value: t.value })) }
+ ];
+ const dynamicSchema = types.reduce((acc, t) => acc.concat(t.schema || []), []);
+ return baseSchema.concat(dynamicSchema, [{ name: 'isIndex', label: 'Index', type: 'switch' }, { name: 'memo', label: 'Memo', type: 'text' }]);
+ };
+
+ const parseOptionsStr = (str) => {
+ if (!str) return undefined;
+ return str.split('\n').map(s => s.trim()).filter(Boolean).map(line => {
+ const idx = line.indexOf(':'); if (idx > -1) return { label: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() }; return line;
+ });
+ };
+ const formatOptionsStr = (opts) => { if (!opts) return ''; return opts.map(o => typeof o === 'object' ? `${o.label}:${o.value}` : o).join('\n'); };
+
+ container.addField = async () => {
+ container.hideColumnMenu();
+ const data = globalThis.NewState({ id: 'c' + Date.now().toString().slice(-4), name: 'New Field', user_type: 'text', decimals: 0, isIndex: false, memo: '', options_str: '' });
+ const d = container.querySelector(`Dialog[id="${container.id}_field_dialog"]`);
+ if (!d) return;
+ Object.assign(d.state, { title: 'Add Field', buttons: ['Cancel', 'Save'] });
+ const form = d.querySelector('AutoForm');
+ if (form) {
+ form.data = data; form.state.schema = getFieldSchema();
+ }
+ d.show();
+ const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result), { once: true }));
+ if (result === 2) {
+ const typeInfo = globalThis.DataTable.getFieldTypes().find(t => t.value === data.user_type);
+ let dbType = typeInfo?.typeForDB || 'v1024'; if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi';
+ const field = { id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } };
+ state.fields = [...state.fields, field]; state.isDirty = true; container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields })); state.list = [...state.list];
+ }
+ };
+
+ container.editField = async () => {
+ if (!state.activeField) return;
+ container.hideColumnMenu();
+ const f = state.activeField; const s = f.settings || {};
+ const data = globalThis.NewState({ id: f.id, name: f.name, memo: f.memo || '', isIndex: !!f.isIndex, user_type: s.formType || 'text', decimals: s.decimals || 0, prefix: s.prefix || '', suffix: s.suffix || '', thousandSep: !!s.thousandSep, labelOn: s.labelOn || '', labelOff: s.labelOff || '', format: s.format || '', placeholder: s.placeholder || '', options_str: formatOptionsStr(s.options) });
+ const d = container.querySelector(`Dialog[id="${container.id}_field_dialog"]`);
+ if (!d) return;
+ Object.assign(d.state, { title: 'Edit Field', buttons: ['Cancel', 'Save'] });
+ const form = d.querySelector('AutoForm');
+ if (form) {
+ form.data = data; form.state.schema = getFieldSchema();
+ }
+ d.show();
+ const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result), { once: true }));
+ if (result === 2) {
+ const idx = state.fields.findIndex(item => item.id === f.id);
+ if (idx !== -1) {
+ const typeInfo = globalThis.DataTable.getFieldTypes().find(t => t.value === data.user_type);
+ let dbType = typeInfo?.typeForDB || 'v1024'; if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi';
+ const updatedField = { ...f, id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { ...f.settings, formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } };
+ state.fields[idx] = updatedField; state.fields = [...state.fields]; state.isDirty = true; container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields })); state.list = [...state.list];
+ }
+ }
+ };
+
+ container.deleteField = async () => {
+ if (!state.activeField) return;
+ container.hideColumnMenu();
+ if (await globalThis.UI.confirm(`Are you sure you want to delete field "${state.activeField.name}"?`)) {
+ const idx = state.fields.findIndex(f => f.id === state.activeField.id);
+ if (idx !== -1) { state.fields.splice(idx, 1); state.fields = [...state.fields]; state.isDirty = true; container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields })); state.list = [...state.list]; }
+ }
+ };
+
+ window.addEventListener('mouseup', selection.endSelect);
+ document.addEventListener('mousedown', e => {
+ const overlay = container.querySelector('.dt-editor-overlay'); const menu = container.querySelector('.dt-column-menu');
+ if (overlay?.style.display !== 'none' && !overlay.contains(e.target)) container.hideEditor(true);
+ if (!container.contains(e.target) && !overlay?.contains(e.target) && !menu?.contains(e.target)) selection.clearAllActive();
+ });
+
+ state._MODE_ICONS = MODE_ICONS;
+}, globalThis.Util.makeDom(/*html*/`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TOP FREQUENT VALUES
+
+
+
+
+
+
+ Clear Filter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`), globalThis.Util.makeDom(/*html*/`
+
+`))
+
+globalThis.DataTable = DataTableConfig;
+export const DataTable = DataTableConfig;
diff --git a/src/index.js b/src/index.js
index 0d69a6c..99d39e3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,746 +1,7 @@
-import { Component, NewState, Util, RefreshState } from '@web/state'
-import { State } from '@web/base'
-import { createPerfMonitor } from './perf.js'
-import { createScrollManager } from './scroll.js'
-import { createSelectionManager } from './selection.js'
+/**
+ * DataTable Project Entry Point
+ * Orchestrates component loading and re-exports public API.
+ */
-// Static configuration maps
-const MODE_MAP = {
- text: ['contains', 'equals', 'starts', 'ends'],
- textarea: ['contains', 'equals', 'starts', 'ends'],
- number: ['=', '>', '<', 'between'],
- date: ['=', '>', '<', 'between'],
- select: ['contains', 'equals'],
- TagsInput: ['contains', 'equals', 'starts', 'ends']
-};
+import './DataTable.js';
-const MODE_ICONS = {
- 'contains': 'bi-search', 'equals': 'bi-distribute-vertical', 'starts': 'bi-align-start', 'ends': 'bi-align-end',
- '=': 'bi-calculator', '>': 'bi-chevron-right', '<': 'bi-chevron-left', 'between': 'bi-arrows-expand'
-};
-
-export const DataTable = {
- _fieldTypes: new Map(),
- registerFieldType: (config) => {
- DataTable._fieldTypes.set(config.value, config);
- },
- getFieldTypes: () => Array.from(DataTable._fieldTypes.values())
-};
-
-// Register Built-in Types
-DataTable.registerFieldType({
- value: 'text', label: '{#Text#}', typeForDB: 'v4096',
- schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="text"' }]
-});
-DataTable.registerFieldType({
- value: 'number', label: '{#Number#}', typeForDB: 'ff',
- schema: [
- { name: 'decimals', label: 'Decimals', type: 'number', setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' },
- { name: 'prefix', label: 'Prefix (e.g. $)', type: 'text', if: 'this.data.user_type=="number"' },
- { name: 'suffix', label: 'Suffix (e.g. %)', type: 'text', if: 'this.data.user_type=="number"' },
- { name: 'thousandSep', label: 'Thousand Sep', type: 'switch', if: 'this.data.user_type=="number"' }
- ],
- formatter: (val, field) => {
- if (val == null || val === '') return '';
- let num = Number(val);
- if (isNaN(num)) return val;
- const s = field.settings || {};
- if (s.decimals !== undefined) num = num.toFixed(s.decimals);
- let str = String(num);
- if (s.thousandSep) {
- const parts = str.split('.');
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
- str = parts.join('.');
- }
- return (s.prefix || '') + str + (s.suffix || '');
- }
-});
-DataTable.registerFieldType({
- value: 'select', label: '{#Single Select#}', typeForDB: 'v1024',
- schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="select"' }],
- formatter: (val, field) => {
- if (val == null || val === '') return '';
- const opts = field.settings?.options || [];
- const opt = opts.find(o => typeof o === 'object' ? o.value == val : o == val);
- return opt ? (typeof opt === 'object' ? opt.label : opt) : val;
- }
-});
-DataTable.registerFieldType({
- value: 'checkbox', label: '{#Multi Select#}', typeForDB: 'v4096',
- schema: [{ name: 'options_str', label: 'Options', type: 'textarea', setting: { rows: 3, placeholder: 'Label:Value per line' }, if: 'this.data.user_type=="checkbox"' }],
- formatter: (val, field) => {
- if (!Array.isArray(val)) return val == null ? '' : String(val);
- const opts = field.settings?.options || [];
- return val.map(v => {
- const opt = opts.find(o => typeof o === 'object' ? o.value == v : o == v);
- return opt ? (typeof opt === 'object' ? opt.label : opt) : v;
- }).join(', ');
- }
-});
-DataTable.registerFieldType({
- value: 'switch', label: '{#Switch#}', typeForDB: 'b',
- schema: [
- { name: 'labelOn', label: 'Label On', type: 'text', if: 'this.data.user_type=="switch"' },
- { name: 'labelOff', label: 'Label Off', type: 'text', if: 'this.data.user_type=="switch"' }
- ],
- formatter: (val, field) => {
- const s = field.settings || {};
- return val ? (s.labelOn || 'Yes') : (s.labelOff || 'No');
- }
-});
-DataTable.registerFieldType({
- value: 'datetime', label: '{#DateTime#}', typeForDB: 'dt',
- schema: [{ name: 'format', label: 'Format', type: 'text', setting: { placeholder: 'YYYY-MM-DD' }, if: 'this.data.user_type=="datetime"' }]
-});
-DataTable.registerFieldType({
- value: 'textarea', label: '{#Long Text#}', typeForDB: 't',
- schema: [{ name: 'placeholder', label: 'Placeholder', type: 'text', if: 'this.data.user_type=="textarea"' }]
-});
-
-Component.register('DataTable', container => {
- if (!container.state) container.state = NewState({})
- const state = container.state
-
- Object.assign(state, {
- list: [], fields: [], _renderedList: [],
- prevHeight: 0, postHeight: 0, _listStartIndex: 0,
- selectedRowCount: 0,
- _originalList: [],
- sortConfig: { fieldId: null, direction: null },
- filterConfig: {}, // fieldId -> { mode, value, value2, selectedValues: [] }
- activeFieldId: null,
- activeField: null,
- activeModes: [],
- _columnStats: {},
- _internalUpdate: false,
- _appliedHash: '',
- _fieldsDirty: false,
- _masterCellNodes: null,
- isDirty: false,
- isBulkEdit: null
- })
-
- const perf = createPerfMonitor();
- state.perf = perf.stats;
- const selection = createSelectionManager(container, state);
- const scroll = createScrollManager(container, state, () => selection.applySelectionUI());
-
- const menuNode = container.querySelector('.dt-column-menu');
- if (menuNode) menuNode._thisObj = container;
-
- container.onColumnResizing = (field, e) => container.style.setProperty(`--w-${field.id}`, e.detail.newSize + 'px');
- container.onColumnResize = (field, e) => {
- const idx = state.fields.findIndex(f => f.id === field.id);
- if (idx !== -1) { state.fields[idx].width = e.detail.newSize; state.fields = [...state.fields]; }
- };
-
- let _editorOverlay, currentEditingNode = null;
-
- container.format = (val, field) => {
- if (field.formatter) return field.formatter(val, field);
- const typeInfo = DataTable._fieldTypes.get(field.settings?.formType || field.type || 'text');
- if (typeInfo && typeInfo.formatter) return typeInfo.formatter(val, field);
- return val == null ? '' : (typeof val === 'object' ? JSON.stringify(val) : String(val));
- };
-
- container.onScroll = () => {
- perf.onScroll(); scroll.refresh();
- container.hideColumnMenu();
- const prev = container.querySelector('.dt-spacer-prev'), post = container.querySelector('.dt-spacer-post');
- if (prev) { prev.style.height = (state.prevHeight || 0) + 'px'; prev.style.display = state.prevHeight > 0 ? 'block' : 'none'; }
- if (post) { post.style.height = (state.postHeight || 0) + 'px'; post.style.display = state.postHeight > 0 ? 'block' : 'none'; }
- };
-
- container.applySortFilter = (options = {}) => {
- if (state._internalUpdate) return;
- const targetFilters = { ...state.filterConfig, ...(options.filters || {}) };
- const targetSort = options.sort !== undefined ? (options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null }) : state.sortConfig;
- const currentHash = JSON.stringify({ s: targetSort, f: targetFilters });
- if (state._appliedHash === currentHash && !options.force) return;
-
- state._internalUpdate = true;
- let list = [...(state._originalList || [])];
- Object.keys(targetFilters).forEach(fieldId => {
- const filter = targetFilters[fieldId];
- if (!filter) return;
- const { mode = 'contains', value, value2, selectedValues } = filter;
- if (selectedValues?.length > 0) { list = list.filter(item => selectedValues.includes(String(item[fieldId] ?? ''))); return; }
- if (value === '' || value == null) return;
- const lowV = String(value).toLowerCase(), n1 = Number(value), n2 = Number(value2);
- list = list.filter(item => {
- const iv = item[fieldId], sv = String(iv ?? '').toLowerCase();
- switch (mode) {
- case 'contains': return sv.includes(lowV);
- case 'equals': return sv === lowV;
- case 'starts': return sv.startsWith(lowV);
- case 'ends': return sv.endsWith(lowV);
- case '>': return Number(iv) > n1;
- case '<': return Number(iv) < n1;
- case '=': return Number(iv) === n1;
- case 'between': return Number(iv) >= n1 && Number(iv) <= n2;
- default: return sv.includes(lowV);
- }
- });
- });
- if (targetSort.fieldId && targetSort.direction) {
- list.sort((a, b) => {
- let va = a[targetSort.fieldId], vb = b[targetSort.fieldId];
- if (va === vb) return 0;
- const res = va > vb ? 1 : -1;
- return targetSort.direction === 'asc' ? res : -res;
- });
- }
- state._appliedHash = currentHash; state.sortConfig = targetSort; state.list = list; state._internalUpdate = false;
- };
-
- container.showColumnMenu = (field, e) => {
- e.stopPropagation();
- const btn = e.currentTarget, menu = container.querySelector('.dt-column-menu');
- const type = field.type || 'text';
- state.activeModes = MODE_MAP[type] || (['boolean', 'switch', 'checkbox', 'radio'].includes(type) ? [] : MODE_MAP.text);
- if (!state.filterConfig[field.id]) {
- state.filterConfig[field.id] = { mode: state.activeModes[0] || 'contains', value: '', selectedValues: [] };
- }
- state.activeField = field; state.activeFieldId = field.id;
- menu.style.display = 'block';
- const cellNode = btn.closest('.dt-cell');
- const rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
- const menuWidth = menu.offsetWidth || 260;
- let leftPos = rect.right - rootRect.left - menuWidth;
- if (leftPos < 0) leftPos = Math.max(0, rect.left - rootRect.left);
- menu.style.left = leftPos + 'px';
- menu.style.top = (rect.bottom - rootRect.top + 5) + 'px';
-
- const onGlobalClick = (ev) => {
- if (menu.contains(ev.target) || btn.contains(ev.target)) return;
- container.hideColumnMenu(); container.applySortFilter();
- document.removeEventListener('mousedown', onGlobalClick);
- };
- document.addEventListener('mousedown', onGlobalClick);
- setTimeout(() => menu.querySelector('input')?.focus(), 50);
- };
-
- container.toggleSelectedValue = (val) => {
- const filter = state.filterConfig[state.activeFieldId];
- if (!filter) return;
- const idx = filter.selectedValues.indexOf(val);
- if (idx === -1) filter.selectedValues.push(val); else filter.selectedValues.splice(idx, 1);
- state.filterConfig = { ...state.filterConfig };
- container.applySortFilter();
- };
-
- container.filterOnlyThis = (val) => {
- state.filterConfig[state.activeFieldId] = { mode: 'contains', value: '', selectedValues: [String(val)] };
- state.filterConfig = { ...state.filterConfig };
- container.applySortFilter();
- };
-
- container.hideColumnMenu = () => {
- const menu = container.querySelector('.dt-column-menu');
- if (menu) menu.style.display = 'none';
- };
-
- container.setSort = (dir) => {
- const newDir = state.sortConfig.direction === dir && state.sortConfig.fieldId === state.activeFieldId ? null : dir;
- container.applySortFilter({ sort: newDir });
- };
-
- container.clearColumnSettings = () => {
- if (state.activeFieldId) {
- delete state.filterConfig[state.activeFieldId];
- state.filterConfig = { ...state.filterConfig };
- container.applySortFilter();
- }
- };
-
- container._initRow = (rowNode) => {
- const row = rowNode._ref?.item;
- if (row && row._editingF === undefined) {
- Object.defineProperty(row, '_editingF', { set: (v) => { if (v === null) container.hideEditor(true); }, configurable: true });
- }
- Array.from(rowNode.children).forEach(cell => {
- const fIdx = parseInt(cell.dataset.fidx);
- if (!isNaN(fIdx)) cell._refExt = { f: state.fields[fIdx], fIdx: fIdx };
- });
- };
-
- state.__watch('fields', fields => {
- if (!fields) return;
- state._fieldsDirty = true;
- state._masterCellNodes = null; // Force template rebuild
- container.style.setProperty('--dt-grid-template', fields.map(f => `var(--w-${f.id}, ${(f.settings?.width || f.width) || 150}px)`).join(' '));
- container.style.setProperty('--dt-row-width', fields.reduce((sum, f) => sum + ((f.settings?.width || f.width) || 150), 0) + 'px');
-
- let leftSum = 0;
- fields.forEach(f => {
- const pinned = f.settings?.pinned || f.pinned;
- if (pinned === 'left') {
- container.style.setProperty(`--l-${f.id}`, leftSum + 'px');
- leftSum += ((f.settings?.width || f.width) || 150);
- }
- });
- let rightSum = 0;
- [...fields].reverse().forEach(f => {
- const pinned = f.settings?.pinned || f.pinned;
- if (pinned === 'right') {
- container.style.setProperty(`--r-${f.id}`, rightSum + 'px');
- rightSum += ((f.settings?.width || f.width) || 150);
- }
- });
- });
-
- state.__watch('list', list => {
- if (state._fieldsDirty) {
- state._fieldsDirty = false;
- const fieldTemplate = container.querySelector('.dt-body template[index="rIdx"]')?.content.querySelector('template[as="f"]');
- if (fieldTemplate) {
- const masters = state._masterCellNodes || (state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map(n => n.cloneNode(true)));
- fieldTemplate.removeAttribute('$each'); fieldTemplate.setAttribute('$if', 'true'); fieldTemplate.content.textContent = '';
- state.fields.forEach((f, fIdx) => masters.forEach(master => {
- const clone = master.cloneNode(true);
- if (clone.nodeType === 1) {
- clone.dataset.fidx = fIdx;
- const pinned = f.settings?.pinned || f.pinned;
- if (pinned) {
- clone.classList.add('pinned-' + pinned);
- clone.style.position = 'sticky';
- clone.style.zIndex = '1';
- clone.style.backgroundColor = 'inherit';
- if (pinned === 'left') {
- clone.style.left = `var(--l-${f.id})`;
- clone.style.borderRight = '1px solid var(--bs-border-color)';
- clone.style.boxShadow = '2px 0 5px -2px rgba(0,0,0,0.1)';
- } else {
- clone.style.right = `var(--r-${f.id})`;
- clone.style.borderLeft = '1px solid var(--bs-border-color)';
- clone.style.boxShadow = '-2px 0 5px -2px rgba(0,0,0,0.1)';
- }
- }
- }
- fieldTemplate.content.appendChild(clone);
- }));
- }
- }
- if (!state._internalUpdate) {
- state._originalList = [...(list || [])];
- setTimeout(() => {
- const stats = {};
- state.fields.forEach(f => {
- const counts = {};
- state._originalList.forEach(item => { const val = item[f.id], key = (val == null || val === '') ? '' : String(val); counts[key] = (counts[key] || 0) + 1; });
- stats[f.id] = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([val, count]) => ({ val, count }));
- });
- state._columnStats = stats;
- }, 200);
- }
- scroll.init(); scroll.reset(list);
- });
-
- container.editCell = (row, field, cellNode) => {
- const overlay = container.querySelector('.dt-editor-overlay'), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
- currentEditingNode = cellNode;
- const formType = field.settings?.formType || field.type || 'text';
- const form = overlay.querySelector('AutoForm');
- if (form) {
- // row 已经是 Proxy (NewState),直接赋值即可实现双向同步
- form.data = row;
- form.state.schema = [{ ...field, type: formType, options: field.settings?.options || field.options, name: field.id, label: '' }];
- RefreshState(form);
- }
- Object.assign(overlay.style, {
- display: 'flex', left: (rect.left - rootRect.left) + 'px', top: (rect.top - rootRect.top) + 'px',
- width: (formType === 'textarea' || formType === 'TagsInput' ? Math.max(rect.width, 300) : rect.width) + 'px',
- height: (formType === 'textarea' || formType === 'TagsInput' ? 'auto' : rect.height + 'px')
- });
- setTimeout(() => overlay.querySelector('input, textarea, select, .form-control')?.focus(), 30);
- };
-
- container.hideEditor = (save = true) => {
- if (!_editorOverlay) _editorOverlay = container.querySelector('.dt-editor-overlay');
- if (!_editorOverlay || _editorOverlay.style.display === 'none') return;
- const form = _editorOverlay.querySelector('AutoForm');
- if (save && form && form.data) {
- const input = _editorOverlay.querySelector('input:focus, select:focus, textarea:focus');
- if (input) input.dispatchEvent(new Event(input.type === 'number' || input.tagName === 'SELECT' ? 'change' : 'input', { bubbles: true }));
- RefreshState(form);
-
- // 手动同步数据回原始行 (解决 Proxy 隔离问题)
- const schema = form.state.schema || [];
- schema.forEach(field => {
- const row = currentEditingNode?.closest('.dt-row')?._ref?.item;
- if (row) row[field.name] = form.data[field.name];
- });
-
- if (state.isBulkEdit) {
- const { minRow, maxRow, fIdx } = state.isBulkEdit;
- const field = state.fields[fIdx];
- const newValue = form.data[field.id];
- for (let i = minRow; i <= maxRow; i++) {
- if (state.list[i]) {
- state.list[i][field.id] = newValue;
- }
- }
- state.list = [...state.list];
- }
-
- if (currentEditingNode) RefreshState(currentEditingNode);
- state.isDirty = true;
- }
- _editorOverlay.style.display = 'none';
- if (form) { form.state.schema = []; form.data = null; }
- currentEditingNode = null;
- state.isBulkEdit = null;
- container.focus();
- };
-
- container.onMainMouseDown = e => {
- const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row');
- if (!row || row.classList.contains('dt-header-row')) return;
- const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell);
- const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
- selection.startSelect(rIdx + state._listStartIndex, fIdx, e);
- };
-
- container.onMainMouseOver = e => {
- if (state.isSelecting) {
- const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row');
- if (row && !row.classList.contains('dt-header-row')) {
- const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell);
- const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
- selection.updateSelect(rIdx + state._listStartIndex, fIdx);
- }
- }
- };
-
- container.onMainDblClick = e => {
- const cell = e.target.closest('.dt-cell'), row = cell?.closest('.dt-row');
- if (row && !row.classList.contains('dt-header-row')) {
- const item = row._ref?.item, fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll('.dt-cell')).indexOf(cell);
- const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
- const absoluteRow = rIdx + state._listStartIndex;
-
- if (item && state.fields[fIdx]) {
- const bounds = selection.getSelectionBounds();
- if (bounds && absoluteRow >= bounds.minRow && absoluteRow <= bounds.maxRow && fIdx >= bounds.minCol && fIdx <= bounds.maxCol) {
- const affectedRows = bounds.maxRow - bounds.minRow + 1;
- if (affectedRows > 1) {
- state.isBulkEdit = { ...bounds, fIdx };
- if (globalThis.UI?.toast) UI.toast(`Bulk Edit: Updating ${affectedRows} rows in column "${state.fields[fIdx].name}"`, { type: 'warning' });
- }
- }
- container.editCell(item, state.fields[fIdx], cell);
- }
- }
- };
-
- container.addRow = () => {
- const newRow = {};
- state.fields.forEach(f => newRow[f.id] = '');
- state._originalList.push(newRow);
- state.list = [...state._originalList]; // Apply to current view too
- state.isDirty = true;
- setTimeout(() => {
- scroll.reset(state.list);
- container.querySelector('.dt-main').scrollTop = container.querySelector('.dt-main').scrollHeight;
- }, 50);
- };
-
- container.deleteSelectedRow = async () => {
- const bounds = selection.getSelectionBounds();
- if (!bounds) return;
- const count = bounds.maxRow - bounds.minRow + 1;
- if (await UI.confirm(`Are you sure you want to delete ${count} row(s)?`)) {
- const rMin = bounds.minRow, rMax = bounds.maxRow;
- const removedItems = state.list.slice(rMin, rMax + 1);
- state.list = state.list.filter((_, i) => !(i >= rMin && i <= rMax));
- state._originalList = state._originalList.filter(item => !removedItems.includes(item));
- state.isDirty = true;
- selection.clearAllActive();
- container.dispatchEvent(new CustomEvent('remove', { detail: { items: removedItems } }));
- }
- };
-
- container.saveChanges = () => {
- container.dispatchEvent(new CustomEvent('save', { detail: { list: state._originalList, fields: state.fields } }));
- state.isDirty = false;
- };
-
- const getFieldSchema = () => {
- const types = DataTable.getFieldTypes();
- const baseSchema = [
- { name: 'id', label: 'Field ID', type: 'text', setting: { required: true, placeholder: 'e.g. user_name' } },
- { name: 'name', label: 'Display Name', type: 'text', setting: { required: true, placeholder: 'e.g. 用户名' } },
- { name: 'user_type', label: 'Field Type', type: 'select', options: types.map(t => ({ label: t.label, value: t.value })) }
- ];
- const dynamicSchema = types.reduce((acc, t) => acc.concat(t.schema || []), []);
- return baseSchema.concat(dynamicSchema, [
- { name: 'isIndex', label: 'Index', type: 'switch' },
- { name: 'memo', label: 'Memo', type: 'text' }
- ]);
- };
-
- const parseOptionsStr = (str) => {
- if (!str) return undefined;
- return str.split('\n').map(s => s.trim()).filter(Boolean).map(line => {
- const idx = line.indexOf(':');
- if (idx > -1) return { label: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() };
- return line;
- });
- };
- const formatOptionsStr = (opts) => {
- if (!opts) return '';
- return opts.map(o => typeof o === 'object' ? `${o.label}:${o.value}` : o).join('\n');
- };
-
- container.addField = async () => {
- container.hideColumnMenu();
- const data = NewState({ id: 'c' + Date.now().toString().slice(-4), name: 'New Field', user_type: 'text', decimals: 0, isIndex: false, memo: '', options_str: '' });
-
- const d = document.body.appendChild(document.createElement('Dialog'));
- await new Promise(r => setTimeout(r, 0));
- Object.assign(d.state, { title: 'Add Field', buttons: ['Cancel', 'Save'] });
- RefreshState(d);
-
- const body = d.querySelector('.modal-body');
- const form = body.appendChild(document.createElement('AutoForm'));
- form.setAttribute('nobutton', '');
- RefreshState(form);
- form.data = data;
- form.state.schema = getFieldSchema();
-
- form.addEventListener('change', (e) => e.stopPropagation());
-
- d.show();
- const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result)));
- if (result === 2) {
- const typeInfo = DataTable._fieldTypes.get(data.user_type);
- let dbType = typeInfo?.typeForDB || 'v1024';
- if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi';
-
- const field = {
- id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex,
- type: dbType,
- settings: {
- formType: data.user_type,
- decimals: data.decimals,
- prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep,
- labelOn: data.labelOn, labelOff: data.labelOff, format: data.format,
- placeholder: data.placeholder,
- options: parseOptionsStr(data.options_str)
- }
- };
- state.fields = [...state.fields, field];
- state.isDirty = true;
- container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields }));
- state.list = [...state.list];
- }
- d.remove();
- };
-
- container.editField = async () => {
- if (!state.activeField) return;
- container.hideColumnMenu();
- const f = state.activeField;
- const s = f.settings || {};
- const data = NewState({
- id: f.id, name: f.name, memo: f.memo || '', isIndex: !!f.isIndex,
- user_type: s.formType || 'text',
- decimals: s.decimals || 0,
- prefix: s.prefix || '', suffix: s.suffix || '', thousandSep: !!s.thousandSep,
- labelOn: s.labelOn || '', labelOff: s.labelOff || '', format: s.format || '',
- placeholder: s.placeholder || '',
- options_str: formatOptionsStr(s.options)
- });
-
- const d = document.body.appendChild(document.createElement('Dialog'));
- await new Promise(r => setTimeout(r, 0));
- Object.assign(d.state, { title: 'Edit Field', buttons: ['Cancel', 'Save'] });
- RefreshState(d);
-
- const body = d.querySelector('.modal-body');
- const form = body.appendChild(document.createElement('AutoForm'));
- form.setAttribute('nobutton', '');
- RefreshState(form);
- form.data = data;
- form.state.schema = getFieldSchema();
-
- form.addEventListener('change', (e) => e.stopPropagation());
-
- d.show();
- const result = await new Promise(resolve => d.addEventListener('change', e => resolve(d.result)));
- if (result === 2) {
- const idx = state.fields.findIndex(item => item.id === f.id);
- if (idx !== -1) {
- const typeInfo = DataTable._fieldTypes.get(data.user_type);
- let dbType = typeInfo?.typeForDB || 'v1024';
- if (data.user_type === 'number') dbType = data.decimals > 0 ? 'ff' : 'bi';
-
- const updatedField = {
- ...f, id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex,
- type: dbType,
- settings: {
- ...f.settings,
- formType: data.user_type,
- decimals: data.decimals,
- prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep,
- labelOn: data.labelOn, labelOff: data.labelOff, format: data.format,
- placeholder: data.placeholder,
- options: parseOptionsStr(data.options_str)
- }
- };
- state.fields[idx] = updatedField;
- state.fields = [...state.fields];
- state.isDirty = true;
- container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields }));
- state.list = [...state.list];
- }
- }
- d.remove();
- };
-
- container.deleteField = async () => {
- if (!state.activeField) return;
- container.hideColumnMenu();
- if (await UI.confirm(`Are you sure you want to delete field "${state.activeField.name}"?`)) {
- const idx = state.fields.findIndex(f => f.id === state.activeField.id);
- if (idx !== -1) {
- state.fields.splice(idx, 1);
- state.fields = [...state.fields];
- state.isDirty = true;
- container.dispatchEvent(new CustomEvent('savefields', { detail: state.fields }));
- state.list = [...state.list]; // Trigger body refresh
- }
- }
- };
-
- window.addEventListener('mouseup', selection.endSelect);
- document.addEventListener('mousedown', e => {
- const overlay = container.querySelector('.dt-editor-overlay');
- const menu = container.querySelector('.dt-column-menu');
- if (overlay?.style.display !== 'none' && !overlay.contains(e.target)) container.hideEditor(true);
- if (!container.contains(e.target) && !overlay?.contains(e.target) && !menu?.contains(e.target)) selection.clearAllActive();
- });
-
- // Exposure for templates
- state._MODE_ICONS = MODE_ICONS;
-
- // NOTE: For $class and $style directives, ALWAYS use the template literal syntax:
- // $class="base-class \${condition ? 'active' : ''}"
- // DO NOT use string concatenation like $class="'base-class ' + (condition ? 'active' : '')".
- // Since the HTML is wrapped in backticks (``), remember to escape the dollar sign: \${ }
-}, Util.makeDom(/*html*/`
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
TOP FREQUENT VALUES
-
-
-
-
-
-
- Clear Filter
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`), Util.makeDom(/*html*/`
-
-`))
diff --git a/src/perf.js b/src/perf.js
deleted file mode 100644
index a666365..0000000
--- a/src/perf.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export const createPerfMonitor = () => {
- let enabled = !!window.__DT_PERF_MODE__;
- const stats = { refreshTime: 0, refreshCount: 0, scrollCount: 0, totalNodes: 0 };
-
- if (enabled && !window.__statePerformanceTelemetry) {
- window.__statePerformanceTelemetry = { scanCount: 0, reuseCount: 0, moveCount: 0 };
- }
-
- return {
- get stats() { return stats; },
- enable: () => { enabled = true; },
- disable: () => { enabled = false; },
- onScroll: () => { if (enabled) stats.scrollCount++; },
- startFrame: () => {
- if (!enabled) return null;
- return {
- start: performance.now(),
- scan: window.__statePerformanceTelemetry?.scanCount || 0,
- move: window.__statePerformanceTelemetry?.moveCount || 0,
- reuse: window.__statePerformanceTelemetry?.reuseCount || 0
- };
- },
- endFrame: (startData, renderedCount) => {
- if (!enabled || !startData) return;
- stats.refreshCount++;
- stats.totalNodes += renderedCount;
-
- const elapsed = performance.now() - startData.start;
- stats.refreshTime += elapsed;
-
- const stPerf = window.__statePerformanceTelemetry;
- if (stPerf) {
- const scans = stPerf.scanCount - startData.scan;
- const moves = stPerf.moveCount - startData.move;
- const reuses = stPerf.reuseCount - startData.reuse;
-
- if (scans > 0 || elapsed > 2) {
- console.log(`[DataTable Frame] Time: ${elapsed.toFixed(2)}ms, Scans: ${scans}, Moves: ${moves}, Reuses: ${reuses}, Rows: ${renderedCount}`);
- }
- }
- }
- };
-};
\ No newline at end of file
diff --git a/src/scroll.js b/src/scroll.js
deleted file mode 100644
index 4ee263e..0000000
--- a/src/scroll.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { VirtualScroll } from '@web/base';
-
-export const createScrollManager = (container, state, onRenderedListChange) => {
- const vs = VirtualScroll({ itemHeight: 40 });
- let scrollEl = null;
-
- const refresh = (isLayoutChange = false) => {
- if (!scrollEl) return;
- const res = vs.calc(scrollEl, state.list);
- if (res) {
- // Dirty check: Abort if layout boundaries haven't actually changed
- if (!isLayoutChange &&
- state.prevHeight === res.prevHeight &&
- state.postHeight === res.postHeight &&
- state._listStartIndex === res.listStartIndex &&
- state._renderedList.length === res.renderedList.length) {
- return;
- }
-
- Object.assign(state, {
- prevHeight: res.prevHeight,
- postHeight: res.postHeight,
- _listStartIndex: res.listStartIndex,
- _renderedList: res.renderedList
- });
- onRenderedListChange?.(res.renderedList.length, isLayoutChange);
- }
- };
-
- return {
- init: () => {
- scrollEl = container.querySelector('.dt-main');
- },
- reset: (list) => {
- state._listStartIndex = 0;
- // Optimization: Don't update _renderedList here.
- // vs.init will trigger refresh(true) which does it properly once.
- vs.reset(list, scrollEl || container);
- if (state.list === list) {
- vs.init(list, () => refresh(true));
- }
- },
- refresh,
- onScroll: () => refresh(false)
- };
-};
diff --git a/src/selection.js b/src/selection.js
deleted file mode 100644
index a79b101..0000000
--- a/src/selection.js
+++ /dev/null
@@ -1,242 +0,0 @@
-export const createSelectionManager = (container, state) => {
- let activeBounds = null; // { minRow, maxRow, minCol, maxCol }
- let startCell = null; // { row, col }
- let multiSelections = [];
-
- const isCellSelected = (r, c) => {
- if (activeBounds && r >= activeBounds.minRow && r <= activeBounds.maxRow && c >= activeBounds.minCol && c <= activeBounds.maxCol) return true;
- return multiSelections.some(s => r >= s.minRow && r <= s.maxRow && c >= s.minCol && c <= s.maxCol);
- };
-
- let lastHadSelection = false;
-
- const applySelectionUI = () => {
- /* DEBUG: Performance Isolation Toggle */
- if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return;
-
- let boundMinRow = Infinity, boundMaxRow = -Infinity;
- if (activeBounds) {
- boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
- boundMaxRow = Math.max(boundMaxRow, activeBounds.maxRow);
- }
- multiSelections.forEach(s => {
- boundMinRow = Math.min(boundMinRow, s.minRow);
- boundMaxRow = Math.max(boundMaxRow, s.maxRow);
- });
-
- const hasSelection = boundMinRow !== Infinity;
- if (!hasSelection && !lastHadSelection) return;
- lastHadSelection = hasSelection;
-
- const body = container.querySelector('.dt-body');
- if (!body) return;
-
- const rowNodes = body.querySelectorAll('.dt-body-row');
- rowNodes.forEach(rowNode => {
- const absoluteRow = (rowNode._ref?.rIdx ?? -1) + state._listStartIndex;
- const cells = rowNode.querySelectorAll('.dt-cell');
-
- if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
- cells.forEach(cell => cell.classList.remove('dt-cell-selected'));
- return;
- }
-
- cells.forEach((cell, cIdx) => {
- if (isCellSelected(absoluteRow, cIdx)) {
- cell.classList.add('dt-cell-selected');
- } else {
- cell.classList.remove('dt-cell-selected');
- }
- });
- });
- };
-
- const updateStatus = () => {
- let count = 0;
- if (activeBounds) count += (activeBounds.maxRow - activeBounds.minRow + 1);
- multiSelections.forEach(s => count += (s.maxRow - s.minRow + 1));
- state.selectedRowCount = count;
- };
-
- const clearAllActive = (keepSelection = false) => {
- if (!keepSelection) {
- activeBounds = null;
- startCell = null;
- multiSelections = [];
- applySelectionUI();
- updateStatus();
- }
- };
-
- const startSelect = (row, col, e) => {
- const alreadySelected = isCellSelected(row, col);
- const isRange = (activeBounds && (activeBounds.minRow !== activeBounds.maxRow || activeBounds.minCol !== activeBounds.maxCol)) || multiSelections.length > 0;
-
- if (e.shiftKey && startCell) {
- activeBounds = {
- minRow: Math.min(startCell.row, row),
- maxRow: Math.max(startCell.row, row),
- minCol: Math.min(startCell.col, col),
- maxCol: Math.max(startCell.col, col)
- };
- } else {
- if (alreadySelected && !e.ctrlKey && !e.metaKey) {
- // If clicking inside a range or a single selection, don't clear yet.
- // Mark for potential single-cell toggle-off on mouseup if it was just a single cell.
- if (!isRange) container._potentialCancel = { row, col };
- // else: Keep current selection as is (sticky selection)
- } else {
- if (!e.ctrlKey && !e.metaKey) {
- clearAllActive();
- } else if (activeBounds && !alreadySelected) {
- multiSelections.push(activeBounds);
- }
- startCell = { row, col };
- activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col };
- }
- state.isSelecting = true;
- }
- applySelectionUI();
- updateStatus();
- container.focus();
- };
-
- const updateSelect = (row, col) => {
- if (state.isSelecting && startCell) {
- activeBounds = {
- minRow: Math.min(startCell.row, row),
- maxRow: Math.max(startCell.row, row),
- minCol: Math.min(startCell.col, col),
- maxCol: Math.max(startCell.col, col)
- };
- container._potentialCancel = null; // Dragging means no cancel
- applySelectionUI();
- updateStatus();
- }
- };
-
- const endSelect = () => {
- if (container._potentialCancel) {
- const { row, col } = container._potentialCancel;
- if (isCellSelected(row, col)) {
- clearAllActive();
- }
- container._potentialCancel = null;
- }
- state.isSelecting = false;
- };
-
- const getSelectionBounds = () => {
- if (!activeBounds) return null;
- let minRow = activeBounds.minRow, maxRow = activeBounds.maxRow;
- let minCol = activeBounds.minCol, maxCol = activeBounds.maxCol;
- multiSelections.forEach(s => {
- minRow = Math.min(minRow, s.minRow);
- maxRow = Math.max(maxRow, s.maxRow);
- minCol = Math.min(minCol, s.minCol);
- maxCol = Math.max(maxCol, s.maxCol);
- });
- return { minRow, maxRow, minCol, maxCol };
- };
-
- // Excel-style TSV Copy
- const copy = async () => {
- const bounds = getSelectionBounds();
- if (!bounds) return;
-
- const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map(row => {
- return state.fields.slice(bounds.minCol, bounds.maxCol + 1).map(f => {
- let val = String(row[f.id] ?? '');
- if (val.includes('\t') || val.includes('\n') || val.includes('"')) {
- val = '"' + val.replace(/"/g, '""') + '"';
- }
- return val;
- }).join('\t');
- }).join('\n');
-
- await navigator.clipboard.writeText(text);
- };
-
- // Excel-style TSV Paste (Optimized for targeted refresh)
- const paste = async () => {
- try {
- const text = await navigator.clipboard.readText();
- if (!text) return;
-
- const bounds = getSelectionBounds();
- if (!bounds) return;
-
- // Simple TSV parser
- const rows = text.split(/\r?\n/).filter(line => line.length > 0).map(line => {
- const cells = [];
- let current = '', inQuotes = false;
- for (let i = 0; i < line.length; i++) {
- const char = line[i];
- if (char === '"') {
- if (inQuotes && line[i+1] === '"') { current += '"'; i++; }
- else inQuotes = !inQuotes;
- } else if (char === '\t' && !inQuotes) {
- cells.push(current);
- current = '';
- } else {
- current += char;
- }
- }
- cells.push(current);
- return cells;
- });
-
- const startRow = bounds.minRow;
- const startCol = bounds.minCol;
- const maxRow = bounds.maxRow;
- const maxCol = bounds.maxCol;
- const body = container.querySelector('.dt-body');
- const rowNodes = body ? Array.from(body.childNodes).filter(n => n.classList?.contains('dt-body-row')) : [];
-
- rows.forEach((rowData, rOffset) => {
- const rIdx = startRow + rOffset;
- if (rIdx > maxRow || rIdx >= state.list.length) return;
-
- const rowItem = state.list[rIdx];
- let rowChanged = false;
-
- rowData.forEach((cellData, cOffset) => {
- const cIdx = startCol + cOffset;
- if (cIdx > maxCol || cIdx >= state.fields.length) return;
- const field = state.fields[cIdx];
- rowItem[field.id] = cellData;
- rowChanged = true;
- });
-
- // Targeted DOM refresh to avoid full list scan lag
- if (rowChanged && container.refreshNode) {
- const domNode = rowNodes.find(n => (n._ref?.rIdx ?? -1) + state._listStartIndex === rIdx);
- if (domNode) container.refreshNode(domNode);
- }
- });
- } catch (err) {
- console.error('Paste Error:', err);
- }
- };
-
- const deleteSelected = () => {
- const bounds = getSelectionBounds();
- if (!bounds) return;
- const rMin = bounds.minRow, rMax = bounds.maxRow;
- const newList = state.list.filter((_, i) => !(i >= rMin && i <= rMax));
- state.list = newList;
- clearAllActive();
- };
-
- return {
- applySelectionUI,
- clearAllActive,
- startSelect,
- updateSelect,
- endSelect,
- deleteSelected,
- getSelectionBounds,
- copy,
- paste
- };
-};
diff --git a/test/diagnostic.html b/test/diagnostic.html
index 501be87..238d35b 100644
--- a/test/diagnostic.html
+++ b/test/diagnostic.html
@@ -4,19 +4,12 @@
AutoForm Diagnostic
-
-
+
+
+
diff --git a/test/index.html b/test/index.html
index 096204e..8f749a9 100644
--- a/test/index.html
+++ b/test/index.html
@@ -22,18 +22,15 @@
-
-
+
+
+
+
+
+
diff --git a/test/lib/base.js b/test/lib/base.js
new file mode 100644
index 0000000..01b51ca
--- /dev/null
+++ b/test/lib/base.js
@@ -0,0 +1,939 @@
+(function(global, factory) {
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("@apigo.cc/state")) : typeof define === "function" && define.amd ? define(["exports", "@apigo.cc/state"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoBase = global.ApigoBase || {}, global.ApigoState));
+})(this, function(exports2, state) {
+ "use strict";
+ const HTTP = {
+ get: ({ url, ...opt }) => HTTP.request({ url, method: "GET", ...opt }),
+ post: ({ url, data, ...opt }) => HTTP.request({ url, method: "POST", data, ...opt }),
+ put: ({ url, data, ...opt }) => HTTP.request({ url, method: "PUT", data, ...opt }),
+ delete: ({ url, ...opt }) => HTTP.request({ url, method: "DELETE", ...opt }),
+ head: ({ url, ...opt }) => HTTP.request({ url, method: "HEAD", ...opt }),
+ request: async ({ url, method = "POST", data = void 0, headers = {}, responseType, timeout = 1e4 }) => {
+ var _a;
+ method = method.toUpperCase();
+ const options = { method, signal: (_a = AbortSignal.timeout) == null ? void 0 : _a.call(AbortSignal, timeout) };
+ if (data !== void 0 && method !== "GET" && method !== "HEAD") {
+ if (data instanceof HTMLFormElement) data = new FormData(data);
+ if (data && typeof data === "object" && !(data instanceof FormData) && !(data instanceof ArrayBuffer || ArrayBuffer.isView(data)) && Object.values(data).some((v) => v instanceof File || v instanceof Blob || v instanceof FileList || Array.isArray(v) && v.some((i) => i instanceof File || i instanceof Blob))) {
+ const fd = new FormData();
+ for (const [k, v] of Object.entries(data)) {
+ if (v instanceof FileList || Array.isArray(v)) Array.from(v).forEach((item) => fd.append(k, item));
+ else if (v !== void 0 && v !== null) fd.append(k, v);
+ }
+ data = fd;
+ }
+ if (data instanceof FormData) {
+ delete headers["Content-Type"];
+ } else if (typeof data !== "string" && !(data instanceof ArrayBuffer || ArrayBuffer.isView(data))) {
+ data = JSON.stringify(data);
+ if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
+ }
+ options.body = data;
+ }
+ if (Object.keys(headers).length) options.headers = headers;
+ const response = { error: null, ok: null, status: 0, headers: {}, responseType: "", result: null };
+ try {
+ const resp = await fetch(url, options);
+ Object.assign(response, { ok: resp.ok, status: resp.status, headers: Object.fromEntries(resp.headers.entries()) });
+ if (!responseType) {
+ const contentType = resp.headers.get("Content-Type") || "";
+ if (contentType.includes("application/json")) responseType = "json";
+ else if (/image|video|audio|pdf|zip|octet-stream/.test(contentType)) responseType = "binary";
+ else responseType = "text";
+ response.responseType = responseType;
+ }
+ if (response.ok === false) response.error = (response.statusText || "HTTP " + response.status + " error") + " for " + url;
+ if (responseType === "json") response.result = await resp.json();
+ else response.result = responseType === "binary" ? await resp.arrayBuffer() : await resp.text();
+ } catch (err) {
+ Object.assign(response, { error: err.message || String(err), ok: false });
+ }
+ return response;
+ }
+ };
+ globalThis.HTTP = HTTP;
+ const UI = {};
+ globalThis.Component.register("Toast", (container) => {
+ container.toast = new bootstrap.Toast(container, { autohide: container.state.delay > 0 });
+ globalThis.Util.copyFunction(container, container.toast, "show", "hide");
+ container.addEventListener("show.bs.toast", () => {
+ if (container.state.delay > 0) {
+ let timer;
+ const startTimer = () => {
+ container.state.left = container.state.delay / 1e3;
+ timer = setInterval(() => {
+ if (!container.isConnected || --container.state.left <= 0) clearInterval(timer);
+ }, 1e3);
+ };
+ startTimer();
+ container.addEventListener("mouseenter", () => {
+ clearInterval(timer);
+ container.state.left = void 0;
+ });
+ container.addEventListener("mouseleave", startTimer);
+ }
+ });
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+`
+ ), globalThis.Util.makeDom(
+ /*html*/
+ ``
+ ));
+ UI.toast = function(message, options = {}) {
+ const delay = options.delay ?? 5e3;
+ const t = document.createElement("Toast");
+ t.state = { delay, left: delay ? delay / 1e3 : void 0, type: options.type || "primary", message, buttons: options.buttons || [] };
+ globalThis.$(`[toast-container="${options.container || "default"}"]`).appendChild(t);
+ Promise.resolve().then(() => t.show());
+ };
+ UI.toastConfirm = function(message, options = {}) {
+ return new Promise((resolve) => UI.toast(message, { buttons: ["{#Confirm#}"], ...options }).then((index2) => resolve(index2 === 1)).catch(() => resolve(false)));
+ };
+ globalThis.UI = UI;
+ const AUTOFORM_BLUEPRINT = globalThis.Util.makeDom(
+ /*html*/
+ `
+
+`
+ );
+ const AUTOFORM_STYLE = globalThis.Util.makeDom(
+ /*html*/
+ ``
+ );
+ globalThis.Component.register("AutoForm", (container) => {
+ if (!container.state.schema) container.state.schema = [];
+ const ensureProxy = (v) => v && typeof v === "object" && !v.__isProxy ? globalThis.NewState(v) : v;
+ container.state.__watch("data", (v) => container.data = ensureProxy(v));
+ container.data = ensureProxy(container.state.data || {});
+ container.vertical = container.hasAttribute("vertical");
+ container.horizontal = container.hasAttribute("horizontal");
+ container.inline = container.hasAttribute("inline");
+ container.nobutton = container.hasAttribute("nobutton");
+ container.request = { method: "POST" };
+ container.response = {};
+ container.result = null;
+ container.form = globalThis.$(container, "form");
+ container.submit = (opt = {}) => {
+ var _a, _b;
+ if (!container.form.reportValidity()) return (_b = (_a = globalThis.UI) == null ? void 0 : _a.toast) == null ? void 0 : _b.call(_a, "{#verify failed#}", { type: "danger" });
+ if (!container.dispatchEvent(new CustomEvent("submit", { detail: container.data, cancelable: true, bubbles: false }))) return;
+ const req = { ...container.request, data: container.data, noui: true, ...opt };
+ let task = null;
+ if (container.api) task = container.api.do(req);
+ else if (container.request.url) task = globalThis.HTTP.request(req);
+ else return console.warn("{#please config .api or .request.url to auto submit#}");
+ task.then((resp) => {
+ container.response = resp;
+ container.result = resp.result;
+ if (typeof resp.result === "object" && resp.result.error) throw new Error(resp.result.error);
+ container.dispatchEvent(new CustomEvent("response", { detail: resp, bubbles: false }));
+ }).catch((err) => {
+ var _a2;
+ if ((_a2 = globalThis.UI) == null ? void 0 : _a2.toast) globalThis.UI.toast(err.message, { type: "danger" });
+ container.dispatchEvent(new CustomEvent("error", { detail: err, bubbles: true }));
+ });
+ };
+ container.checkIf = (item) => {
+ if (!item.if) return true;
+ try {
+ const fn = new Function("Hash", "LocalStorage", "State", "item", "data", "return " + item.if);
+ return fn.call(container, globalThis.Hash, globalThis.LocalStorage, globalThis.State, item, container.data);
+ } catch (e) {
+ return false;
+ }
+ };
+ }, AUTOFORM_BLUEPRINT, AUTOFORM_STYLE);
+ const findAnchorInBlueprint = (root) => {
+ let f = root.querySelector("[control-wrapper]");
+ if (f) return f;
+ for (const t of root.querySelectorAll("template")) {
+ f = findAnchorInBlueprint(t.content);
+ if (f) return f;
+ }
+ return null;
+ };
+ const AutoForm = {
+ customTypes: [],
+ register: (name, typeName) => {
+ const type = typeName || name;
+ if (!AutoForm.customTypes.find((t) => t.name === name)) {
+ AutoForm.customTypes.push({ name, typeName: type });
+ AutoForm._addAutoFormComponent(name, type);
+ }
+ },
+ _addAutoFormComponent: (name, type) => {
+ const wrapper = findAnchorInBlueprint(AUTOFORM_BLUEPRINT);
+ if (wrapper) {
+ const node = globalThis.Util.makeDom(`<${name} $if="item.type?.toLowerCase() === '${type.toLowerCase()}'" $name="item.name" $.="item.setting || {}" $bind="this.data[item.name]" class="w-100">${name}>`);
+ wrapper.appendChild(node);
+ }
+ }
+ };
+ globalThis.AutoForm = AutoForm;
+ const VirtualScroll = (options = {}) => {
+ const itemHeights = /* @__PURE__ */ new Map();
+ const groupHeights = /* @__PURE__ */ new Map();
+ let groupItemCount = 1;
+ const avg = globalThis.Util.newAvg();
+ let padTop = 0, rowGap = 0, topMargin = 0, itemMarginTop = null, itemMarginBottom = null, listInited = false;
+ const providedItemHeight = options.itemHeight || null;
+ return {
+ reset: (list, container) => {
+ listInited = false;
+ itemHeights.clear();
+ groupHeights.clear();
+ avg.clear();
+ topMargin = 0;
+ itemMarginTop = null;
+ itemMarginBottom = null;
+ if (!(list == null ? void 0 : list.length)) return [];
+ const size = list.length;
+ groupItemCount = Math.ceil(Math.sqrt(size)) || 10;
+ const style = window.getComputedStyle(container);
+ padTop = parseFloat(style.paddingTop) || 0;
+ rowGap = parseFloat(style.rowGap) || 0;
+ const visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / (providedItemHeight || 32)));
+ return list.slice(0, Math.min(visibleCount * 3, size));
+ },
+ init: (list, refreshCallback) => {
+ if (listInited) return;
+ const size = list.length;
+ let defaultHeight = providedItemHeight || avg.get() || 32;
+ if (size > 0 && typeof list[0] === "object" && list[0] !== null && list[0]._itemHeight) {
+ defaultHeight = list[0]._itemHeight;
+ }
+ avg.add(defaultHeight);
+ if (itemMarginTop === null) {
+ itemMarginTop = 0;
+ itemMarginBottom = 0;
+ }
+ for (let i = 0; i < size; i++) {
+ if (!itemHeights.has(i)) {
+ const ih = typeof list[i] === "object" && list[i] !== null && list[i]._itemHeight ? list[i]._itemHeight : defaultHeight;
+ itemHeights.set(i, ih);
+ }
+ }
+ for (let i = 0; i < size; i += groupItemCount) groupHeights.set(i, Math.min(groupItemCount, size - i) * defaultHeight);
+ listInited = true;
+ refreshCallback();
+ },
+ update: (absoluteIndex, node) => {
+ if (node.offsetHeight === 0) return;
+ if (itemMarginTop === null) {
+ const style = window.getComputedStyle(node);
+ itemMarginTop = parseFloat(style.marginTop) || 0;
+ itemMarginBottom = parseFloat(style.marginBottom) || 0;
+ }
+ if (absoluteIndex === 0 && !topMargin) topMargin = itemMarginTop;
+ const newHeight = node.offsetHeight + itemMarginTop + itemMarginBottom + rowGap;
+ const oldHeight = itemHeights.get(absoluteIndex);
+ if (newHeight !== oldHeight) {
+ itemHeights.set(absoluteIndex, newHeight);
+ avg.add(newHeight);
+ const offset = newHeight - (oldHeight || 0), groupIndex = absoluteIndex - absoluteIndex % groupItemCount;
+ if (groupHeights.has(groupIndex)) groupHeights.set(groupIndex, groupHeights.get(groupIndex) + offset);
+ }
+ },
+ calc: (container, list) => {
+ if (!listInited || !list) return null;
+ const size = list.length;
+ const avgVal = Math.max(16, avg.get() || 32);
+ let visibleCount = Math.max(10, Math.ceil((container.clientHeight || 100) / avgVal));
+ let prev = padTop + topMargin + rowGap, post = 0, status = 0, listStartIndex = 0, listEndIndex = 0;
+ let renderedList = [];
+ const scrollTop = container.scrollTop;
+ let loopCount = 0;
+ for (let i = 0; i < size; i++) {
+ if (++loopCount > size * 2) throw new Error("VirtualScroll infinite loop");
+ if (status === 0) {
+ const gh = groupHeights.get(i);
+ if (gh && prev + gh <= scrollTop && i + groupItemCount < size) {
+ prev += gh;
+ i += groupItemCount - 1;
+ } else {
+ const ih = itemHeights.get(i);
+ if (prev + ih <= scrollTop && i < size - 1) {
+ prev += ih;
+ } else {
+ status = 1;
+ let visibleStartIndex = Math.max(0, i);
+ listStartIndex = Math.max(0, visibleStartIndex - visibleCount);
+ listEndIndex = Math.min(listStartIndex + visibleCount * 3, size);
+ i = listEndIndex - 1;
+ renderedList = list.slice(listStartIndex, listEndIndex);
+ for (let j = listStartIndex; j < visibleStartIndex; j++) prev -= itemHeights.get(j);
+ }
+ }
+ } else if (status === 1) {
+ const gh = groupHeights.get(i);
+ if (gh) {
+ post += gh;
+ i += groupItemCount - 1;
+ } else post += itemHeights.get(i);
+ }
+ }
+ const finalPrevHeight = Math.max(0, prev - padTop - topMargin - rowGap - (listStartIndex > 0 ? rowGap : 0));
+ const finalPostHeight = post > 0 ? Math.max(0, post - 2 * rowGap) : 0;
+ return { prevHeight: finalPrevHeight, postHeight: finalPostHeight, renderedList, listStartIndex };
+ }
+ };
+ };
+ globalThis.VirtualScroll = VirtualScroll;
+ let _mouseMoverMoving = false;
+ let _mouseMoverPos = {};
+ let _mouseMoverEvents = {};
+ const MouseMover = {
+ start: (event, { onmousemove, onmouseup }) => {
+ _mouseMoverPos = { x: event.clientX, y: event.clientY, w: 0, h: 0 };
+ _mouseMoverEvents = { onmousemove, onmouseup };
+ _mouseMoverMoving = true;
+ }
+ };
+ if (typeof document !== "undefined") {
+ document.addEventListener("mouseup", (event) => {
+ var _a;
+ if (!_mouseMoverMoving) return;
+ _mouseMoverMoving = false;
+ (_a = _mouseMoverEvents.onmouseup) == null ? void 0 : _a.call(_mouseMoverEvents, { event, ..._mouseMoverPos });
+ });
+ document.addEventListener("mousemove", (event) => {
+ var _a;
+ if (!_mouseMoverMoving) return;
+ _mouseMoverPos.w = event.clientX - _mouseMoverPos.x;
+ _mouseMoverPos.h = event.clientY - _mouseMoverPos.y;
+ (_a = _mouseMoverEvents.onmousemove) == null ? void 0 : _a.call(_mouseMoverEvents, { event, ..._mouseMoverPos });
+ });
+ }
+ globalThis.MouseMover = MouseMover;
+ const APIComponent = globalThis.Component.register("API", (container) => {
+ container.request = globalThis.NewState({ url: "", method: "GET", headers: {}, data: null, timeout: 1e4, responseType: "" });
+ container.response = globalThis.NewState({ loading: false, ok: null, status: null, error: null, headers: {}, responseType: "", result: null });
+ container.result = globalThis.NewState();
+ container.do = (opt = {}) => {
+ return new Promise((resolve, reject) => {
+ const req = { ...container.request, ...opt };
+ if (!req.url) throw new Error(".url is required");
+ req.headers = { ...container.request.headers, ...opt.headers };
+ container.response.loading = true;
+ globalThis.HTTP.request(req).then((resp) => {
+ Object.keys(resp).forEach((k) => {
+ if (k !== "result") container.response[k] = resp[k];
+ });
+ if (resp.result && typeof resp.result === "object" && container.result && typeof container.result === "object") {
+ Object.assign(container.result, resp.result);
+ } else {
+ container.result = resp.result;
+ }
+ container.response.loading = false;
+ if (resp.ok === false) throw new Error(resp.error);
+ if (typeof resp.result === "object" && resp.result.error) throw new Error(resp.result.error);
+ container.dispatchEvent(new CustomEvent("response", { detail: resp, bubbles: false }));
+ resolve(resp);
+ }).catch((err) => {
+ var _a;
+ if (!opt.noui && ((_a = globalThis.UI) == null ? void 0 : _a.toast)) globalThis.UI.toast(err.message, { type: "danger" });
+ container.dispatchEvent(new CustomEvent("error", { detail: err, bubbles: true }));
+ reject(err);
+ });
+ });
+ };
+ let _autoTimer = null;
+ container.request.__watch(null, () => {
+ if (!container.hasAttribute("auto") || !container.request.url) return;
+ if (_autoTimer) return;
+ _autoTimer = Promise.resolve().then(() => {
+ container.do();
+ _autoTimer = null;
+ });
+ });
+ });
+ globalThis.Component.register("Modal", (container) => {
+ container.modal = new bootstrap.Modal(container);
+ container.addEventListener("bind", (e) => {
+ e.detail ? container.modal.show() : container.modal.hide();
+ });
+ container.addEventListener("hide.bs.modal", () => {
+ var _a;
+ (_a = document.activeElement) == null ? void 0 : _a.blur();
+ container.dispatchEvent(new CustomEvent("change", { bubbles: false, detail: false }));
+ });
+ globalThis.Util.copyFunction(container, container.modal, "show", "hide");
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+`
+ ));
+ globalThis.Component.register("Dialog", globalThis.Component.getSetupFunction("Modal"), globalThis.Util.makeDom(
+ /*html*/
+ `
+
+`
+ ));
+ let _dialogCount = 0;
+ globalThis.UI.showDialog = function({ title = "", message = "", buttons = ["{#Close#}"], type = "body" }) {
+ const d = document.body.appendChild(document.createElement("Dialog"));
+ d.style.zIndex = 2e3 + ++_dialogCount;
+ Promise.resolve().then(() => {
+ Object.assign(d.state, { message, title, type, buttons });
+ d.show();
+ });
+ return new Promise((resolve) => {
+ d.addEventListener("change", (e) => {
+ _dialogCount--;
+ resolve(d.result || 0);
+ d.remove();
+ });
+ });
+ };
+ globalThis.UI.alert = function(message, options = {}) {
+ return globalThis.UI.showDialog({ message, ...options });
+ };
+ globalThis.UI.confirm = function(message, options = {}) {
+ return new Promise((resolve) => globalThis.UI.showDialog({ message, buttons: ["{#Cancel#}", "{#Confirm#}"], ...options }).then((index2) => resolve(index2 >= 2)).catch(() => resolve(false)));
+ };
+ globalThis.Component.register("TagsInput", (container) => {
+ container._thisObj = container;
+ container.addEventListener("bind", (e) => {
+ container.state.tags = Array.isArray(e.detail) ? e.detail : [];
+ });
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+
+
+
+`
+ ), globalThis.Util.makeDom(
+ /*html*/
+ ``
+ ));
+ if (globalThis.AutoForm) {
+ globalThis.AutoForm.register("TagsInput");
+ }
+ globalThis.Component.register("DatePicker", (container) => {
+ container._thisObj = container;
+ container.state = globalThis.NewState({ start: "", end: "" });
+ container.addEventListener("bind", (e) => {
+ var _a, _b, _c;
+ container.state.start = e.detail || "";
+ const form = container.closest("AutoForm");
+ const name = container.getAttribute("name");
+ const item = (_b = (_a = form == null ? void 0 : form.state) == null ? void 0 : _a.schema) == null ? void 0 : _b.find((i) => i.name === name);
+ const rangeEnd = ((_c = item == null ? void 0 : item.setting) == null ? void 0 : _c.rangeEnd) || container.rangeEnd;
+ if (form && rangeEnd) {
+ container.state.end = form.data[rangeEnd] || "";
+ }
+ });
+ Object.defineProperty(container, "isRange", {
+ get: () => {
+ var _a, _b, _c;
+ const form = container.closest("AutoForm");
+ const name = container.getAttribute("name");
+ const item = (_b = (_a = form == null ? void 0 : form.state) == null ? void 0 : _a.schema) == null ? void 0 : _b.find((i) => i.name === name);
+ return !!(((_c = item == null ? void 0 : item.setting) == null ? void 0 : _c.rangeEnd) || container.rangeEnd);
+ }
+ });
+ Object.defineProperty(container, "value", {
+ get: () => container.state.start,
+ set: (v) => {
+ container.state.start = v || "";
+ }
+ });
+ container.updateStart = (val) => {
+ container.state.start = val;
+ container.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: val }));
+ };
+ container.updateEnd = (val) => {
+ var _a, _b, _c;
+ container.state.end = val;
+ const form = container.closest("AutoForm");
+ const name = container.getAttribute("name");
+ const item = (_b = (_a = form == null ? void 0 : form.state) == null ? void 0 : _a.schema) == null ? void 0 : _b.find((i) => i.name === name);
+ const rangeEnd = ((_c = item == null ? void 0 : item.setting) == null ? void 0 : _c.rangeEnd) || container.rangeEnd;
+ if (form && rangeEnd) {
+ form.data[rangeEnd] = val;
+ }
+ };
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+ -
+
+
+
+`
+ ));
+ if (globalThis.AutoForm) {
+ globalThis.AutoForm.register("DatePicker");
+ }
+ globalThis.Component.register("ColorPicker", (container) => {
+ container._thisObj = container;
+ container.state = globalThis.NewState({ value: "#000000" });
+ container.addEventListener("bind", (e) => {
+ container.state.value = e.detail || "#000000";
+ });
+ Object.defineProperty(container, "value", {
+ get: () => container.state.value,
+ set: (v) => {
+ container.state.value = v || "#000000";
+ }
+ });
+ container.updateValue = (val) => {
+ container.state.value = val;
+ container.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: val }));
+ };
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+
+`
+ ));
+ if (globalThis.AutoForm) {
+ globalThis.AutoForm.register("ColorPicker");
+ }
+ const BOOTSTRAP_ICONS = ["alarm", "archive", "arrow-left", "arrow-right", "bag", "bank", "basket", "bell", "bookmark", "box", "briefcase", "calendar", "camera", "cart", "chat", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "clock", "cloud", "code", "collection", "command", "cpu", "credit-card", "cup", "dash", "database", "display", "door-closed", "download", "droplet", "earbuds", "edit", "egg", "eject", "envelope", "eraser", "eye", "file", "filter", "flag", "folder", "gear", "gem", "gift", "graph-up", "grid", "hammer", "hand-thumbs-up", "heart", "house", "image", "inbox", "info-circle", "journal", "key", "laptop", "layers", "layout-text-sidebar-reverse", "lightbulb", "link", "list", "lock", "map", "mic", "moon", "mouse", "music-note", "newspaper", "palette", "paperclip", "pause", "pencil", "person", "phone", "pie-chart", "play", "plus", "printer", "puzzle", "question-circle", "reception-4", "record", "reply", "rss", "save", "search", "send", "server", "share", "shield", "shop", "shuffle", "skip-end", "skip-start", "slash", "sliders", "smartphone", "speaker", "speedometer", "spellcheck", "square", "star", "stickies", "stop", "stopwatch", "suit-heart", "sun", "table", "tag", "tags", "telephone", "terminal", "text-paragraph", "thermometer", "three-dots", "ticket", "tools", "trash", "trophy", "truck", "tv", "umbrella", "unlock", "upload", "vector-pen", "wallet", "watch", "wifi", "window", "wrench", "x", "zoom-in", "zoom-out", "activity", "at", "award", "backspace", "badge-3d", "badge-4k", "badge-8k", "badge-ad", "badge-ar", "badge-cc", "badge-hd", "badge-tm", "badge-vo", "badge-vr", "badge-wc", "bar-chart", "battery", "bicycle", "binoculars", "blockquote-left", "blockquote-right", "book", "bookshelf", "bootstrap", "border-all", "border-bottom", "border-center", "border-inner", "border-left", "border-middle", "border-outer", "border-right", "border-style", "border-top", "border-width", "bounding-box", "box-arrow-down", "box-arrow-in-down", "box-arrow-in-left", "box-arrow-in-right", "box-arrow-in-up", "box-arrow-left", "box-arrow-right", "box-arrow-up", "box-seam", "brightness-alt-high", "brightness-alt-low", "brightness-high", "brightness-low", "broadcast", "brush", "bucket", "bug", "building", "bullseye", "calculator", "calendar-check", "calendar-date", "calendar-day", "calendar-event", "calendar-minus", "calendar-month", "calendar-plus", "calendar-range", "calendar-week", "calendar-x", "calendar2", "calendar3", "calendar4", "camera-reels", "camera-video", "capslock", "card-checklist", "card-heading", "card-image", "card-list", "card-text", "caret-down", "caret-left", "caret-right", "caret-up", "cart-check", "cart-dash", "cart-plus", "cart-x", "cash", "cash-stack", "cast", "chat-dots", "chat-left", "chat-quote", "chat-right", "chat-square", "chat-text", "check-all", "check-circle", "check-square", "circle", "clipboard", "cloud-arrow-down", "cloud-arrow-up", "cloud-check", "cloud-download", "cloud-fog", "cloud-hail", "cloud-lightning", "cloud-minus", "cloud-moon", "cloud-plus", "cloud-rain", "cloud-slash", "cloud-snow", "cloud-sun", "cloud-upload", "clouds", "cloudy", "code-slash", "code-square", "collection-play", "columns", "columns-gap", "compass", "cone", "cone-striped", "controller", "credit-card-2-back", "credit-card-2-front", "crop", "cup-straw", "cursor", "dash-circle", "dash-square", "diagram-2", "diagram-3", "diamond", "dice-1", "dice-2", "dice-3", "dice-4", "dice-5", "dice-6", "disc", "discord", "distribute-horizontal", "distribute-vertical", "door-open", "dot", "droplet-half", "easel", "egg-fried", "emoji-angry", "emoji-dizzy", "emoji-expressionless", "emoji-frown", "emoji-heart-eyes", "emoji-laughing", "emoji-neutral", "emoji-smile", "emoji-sunglasses", "emoji-wink", "envelope-open", "exclamation", "exclamation-circle", "exclamation-diamond", "exclamation-octagon", "exclamation-square", "exclamation-triangle", "eye-slash", "eyedropper", "facebook", "file-arrow-down", "file-arrow-up", "file-binary", "file-break", "file-check", "file-code", "file-diff", "file-earmark", "file-excel", "file-image", "file-lock", "file-medical", "file-minus", "file-music", "file-pdf", "file-person", "file-play", "file-plus", "file-post", "file-ppt", "file-richtext", "file-slides", "file-spreadsheet", "file-text", "file-word", "file-zip", "files", "film", "filter-circle", "filter-left", "filter-right", "filter-square", "fingerprint", "flower1", "flower2", "flower3", "folder-check", "folder-minus", "folder-plus", "folder-symlink", "folder-x", "folder2-open", "fonts", "forward", "front", "fullscreen", "fullscreen-exit", "funnel", "gear-wide", "gender-female", "gender-male", "gender-trans", "geo", "geo-alt", "github", "globe", "google", "graph-down", "grid-1x2", "grid-3x2", "grid-3x3", "grip-horizontal", "grip-vertical", "hand-index", "hand-thumbs-down", "handbag", "hash", "headphones", "headset", "heart-half", "heptagon", "hourglass", "hourglass-bottom", "hourglass-split", "hourglass-top", "house-door", "hr", "hurricane", "image-alt", "images", "infinity", "input-cursor", "instagram", "intersect", "journal-album", "journal-arrow-down", "journal-arrow-up", "journal-bookmark", "journal-check", "journal-code", "journal-medical", "journal-minus", "journal-plus", "journal-richtext", "journal-text", "journal-x", "journals", "justify", "kanban", "keyboard", "ladder", "lamp", "layers-half", "layout-sidebar", "layout-split", "layout-three-columns", "life-preserver", "lightbulb-off", "lightning", "lightning-charge", "link-45deg", "linkedin", "list-check", "list-nested", "list-ol", "list-stars", "list-task", "list-ul", "mailbox", "markdown", "mask", "mastodon", "megaphone", "menu-app", "menu-button", "messenger", "mic-mute", "minecart", "minecart-loaded", "moisture", "mouse2", "mouse3", "music-note-beamed", "music-note-list", "music-player", "node-minus", "node-plus", "nut", "octagon", "option", "outlet", "paint-bucket", "patch-check", "patch-exclamation", "patch-minus", "patch-plus", "patch-question", "pause-btn", "pause-circle", "peace", "pen", "pencil-square", "pentagon", "person-badge", "person-bounding-box", "person-circle", "person-lines-fill", "phone-landscape", "phone-vibrate", "pie-chart-fill", "pin", "pin-angle", "pin-fill", "pin-map", "pip", "play-btn", "play-circle", "plug", "plus-circle", "plus-square", "power", "question", "question-diamond", "question-square", "rainbow", "receipt", "receipt-cutoff", "reception-0", "reception-1", "reception-2", "reception-3", "record-btn", "record-circle", "record2", "recycle", "reddit", "reply-all", "router", "rulers", "safe", "save2", "sd-card", "segmented-nav", "shield-check", "shield-exclamation", "shield-lock", "shield-shaded", "shield-slash", "shift", "signpost", "signpost-2", "signpost-split", "sim", "skip-backward", "skip-forward", "slack", "slash-circle", "slash-square", "smartwatch", "snow", "snow2", "snow3", "sort-alpha-down", "sort-alpha-up", "sort-numeric-down", "sort-numeric-up", "soundwave", "speedometer2", "square-half", "stack", "star-half", "stars", "stop-btn", "stop-circle", "suit-club", "suit-diamond", "suit-spade", "sunglasses", "sunrise", "sunset", "symmetry-horizontal", "symmetry-vertical", "tablet", "tablet-landscape", "telegram", "telephone-forward", "telephone-inbound", "telephone-outbound", "telephone-plus", "telephone-x", "text-center", "text-indent-left", "text-indent-right", "text-left", "text-right", "thermometer-half", "thermometer-high", "thermometer-low", "thermometer-snow", "thermometer-sun", "three-dots-vertical", "toggle-off", "toggle-on", "toggle2-off", "toggle2-on", "tornado", "translate", "trash2", "tree", "truck-flatbed", "tsunami", "type", "type-bold", "type-h1", "type-h2", "type-h3", "type-italic", "type-strikethrough", "type-underline", "ui-checks", "ui-checks-grid", "ui-radios", "ui-radios-grid", "union", "upc", "upc-scan", "view-list", "view-stacked", "vinyl", "voicemail", "volume-down", "volume-mute", "volume-off", "volume-up", "vr", "wallet2", "water", "whatsapp", "wifi-1", "wifi-2", "wifi-off", "wind", "window-dock", "window-sidebar", "x-circle", "x-diamond", "x-octagon", "x-square", "youtube"];
+ globalThis.Component.register("IconPicker", (container) => {
+ container._thisObj = container;
+ container.state = globalThis.NewState({ value: "", search: "", open: false });
+ container.addEventListener("bind", (e) => {
+ container.state.value = e.detail || "";
+ });
+ Object.defineProperty(container, "value", {
+ get: () => container.state.value,
+ set: (v) => {
+ container.state.value = v || "";
+ }
+ });
+ Object.defineProperty(container, "filteredIcons", {
+ get: () => {
+ var _a;
+ const s = ((_a = container.state.search) == null ? void 0 : _a.toLowerCase()) || "";
+ return BOOTSTRAP_ICONS.filter((i) => i.includes(s));
+ }
+ });
+ container.selectIcon = (icon) => {
+ container.state.value = icon;
+ container.state.open = false;
+ container.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: icon }));
+ };
+ container.toggle = () => {
+ container.state.open = !container.state.open;
+ if (container.state.open) {
+ setTimeout(() => {
+ var _a;
+ (_a = globalThis.$(container, "input")) == null ? void 0 : _a.focus();
+ }, 10);
+ }
+ };
+ const onGlobalClick = (e) => {
+ if (!container.contains(e.target)) {
+ container.state.open = false;
+ }
+ };
+ window.addEventListener("click", onGlobalClick);
+ container.addEventListener("remove", () => window.removeEventListener("click", onGlobalClick));
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+
+
+
+
+
+
No icons found
+
+
+
+`
+ ), globalThis.Util.makeDom(
+ /*html*/
+ ``
+ ));
+ if (globalThis.AutoForm) {
+ globalThis.AutoForm.register("IconPicker");
+ }
+ globalThis.Component.register("List", (container) => {
+ container.mode = container.getAttribute("mode") || "normal";
+ container.fast = container.hasAttribute("fast");
+ container.collapsible = container.hasAttribute("collapsible");
+ const padTopEl = container.fast ? container.querySelector(".vs-pad-top") : null;
+ const padBottomEl = container.fast ? container.querySelector(".vs-pad-bottom") : null;
+ const defaultSets = {
+ idfield: "id",
+ labelfield: "label",
+ summaryfield: "summary",
+ groupidfield: "id",
+ grouplabelfield: "label",
+ groupsummaryfield: "summary",
+ groupfield: "group",
+ parentfield: "parent",
+ groupicon: "folder",
+ itemicon: "file"
+ };
+ container.collapsed = globalThis.NewState({});
+ container.state.renderedList = [];
+ const updateFlatList = () => {
+ globalThis.Util.updateDefaults(container, defaultSets);
+ const list = container.state.list || [], flatList = [];
+ if (container.mode === "group") {
+ const itemMap = {};
+ list.forEach((item) => {
+ var _a;
+ return (itemMap[_a = item[container.groupfield]] ?? (itemMap[_a] = [])).push(item);
+ });
+ (container.state.groups || []).forEach((group) => {
+ flatList.push({ type: "group", ...group });
+ const items = itemMap[group[container.groupidfield]];
+ if (items) items.forEach((item) => flatList.push({ type: "item", ...item }));
+ });
+ } else if (container.mode === "tree") {
+ const childrenMap = {};
+ list.forEach((item) => {
+ var _a;
+ return (childrenMap[_a = item[container.parentfield] || ""] ?? (childrenMap[_a] = [])).push(item);
+ });
+ const traverse = (items, level, parents) => items.forEach((item) => {
+ var _a;
+ const id = item[container.idfield], hasChildren = !!((_a = childrenMap[id]) == null ? void 0 : _a.length);
+ const isCollapsed = container.collapsed[id];
+ flatList.push({ type: "item", ...item, _level: level, _hasChildren: hasChildren, _parents: parents });
+ if (hasChildren && !isCollapsed) traverse(childrenMap[id], level + 1, [...parents, id]);
+ });
+ traverse(childrenMap[""] || [], 0, []);
+ } else list.forEach((item) => flatList.push({ type: "item", ...item }));
+ container.state.flatList = flatList;
+ };
+ container.state.__watch("list", updateFlatList);
+ const vs = container.fast ? globalThis.VirtualScroll() : null;
+ let refreshing = false;
+ container.refresh = () => {
+ if (!container.fast || refreshing) return;
+ refreshing = true;
+ try {
+ const res = vs.calc(container, container.state.flatList);
+ if (res) {
+ if (padTopEl) padTopEl.style.height = `${res.prevHeight}px`;
+ if (padBottomEl) padBottomEl.style.height = `${res.postHeight}px`;
+ container.state.listStartIndex = res.listStartIndex;
+ container.state.renderedList = res.renderedList;
+ }
+ } finally {
+ setTimeout(() => {
+ refreshing = false;
+ }, 0);
+ }
+ };
+ container.onItemUpdate = (index2, node) => {
+ if (container.fast) vs.update(index2 + (container.state.listStartIndex || 0), node);
+ };
+ container.state.__watch("flatList", (flatList) => {
+ if (container.fast) {
+ if (padTopEl) padTopEl.style.height = "0px";
+ if (padBottomEl) padBottomEl.style.height = "0px";
+ container.state.listStartIndex = 0;
+ container.state.renderedList = vs.reset(flatList, container) || [];
+ setTimeout(() => {
+ if (container.state.flatList === flatList) vs.init(flatList, container.refresh);
+ });
+ } else container.state.renderedList = flatList;
+ });
+ container.selectItem = (item, index2) => {
+ if (container.hasAttribute("auto-select")) container.state.selectedItem = container.state.selectedItem === item[container.idfield] ? null : item[container.idfield];
+ container.dispatchEvent(new CustomEvent("itemclick", { bubbles: false, detail: { item, index: index2 + (container.fast ? container.state.listStartIndex || 0 : 0) } }));
+ };
+ container.selectGroup = (item, index2) => {
+ if (container.hasAttribute("auto-select-group")) container.state.selectedGroup = container.state.selectedGroup === item[container.groupidfield] ? null : item[container.groupidfield];
+ container.dispatchEvent(new CustomEvent("groupclick", { bubbles: false, detail: { item, index: index2 } }));
+ };
+ container.toggleCollapse = (item) => {
+ if (container.collapsible && item._hasChildren) {
+ container.collapsed[item[container.idfield]] = !container.collapsed[item[container.idfield]];
+ updateFlatList();
+ }
+ };
+ updateFlatList();
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ ));
+ globalThis.Component.register("Nav", (container) => {
+ container.vertical = container.hasAttribute("vertical");
+ container.click = (item, noselect) => {
+ if (!item.noselect && !noselect) globalThis.Hash.nav = item.name;
+ container.dispatchEvent(new CustomEvent("nav", { detail: { item }, bubbles: false }));
+ };
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ ));
+ globalThis.Component.register("Resizer", (container) => {
+ container.isVertical = container.hasAttribute("vertical");
+ const min = parseInt(container.getAttribute("min")) || 10;
+ const max = parseInt(container.getAttribute("max")) || 1e3;
+ const target = container.target || container.previousElementSibling;
+ container.addEventListener("bind", (e) => {
+ if (e.detail !== void 0 && e.detail !== null) {
+ target.style[container.isVertical ? "height" : "width"] = e.detail + "px";
+ }
+ });
+ const getSize = (startSize, w, h) => {
+ const newSize = startSize + (container.isVertical ? h : w);
+ return newSize < min ? min : newSize > max ? max : newSize;
+ };
+ container.addEventListener("mousedown", (event) => {
+ const startSize = container.isVertical ? target.offsetHeight : target.offsetWidth;
+ globalThis.MouseMover.start(event, {
+ onmousemove: ({ w, h }) => {
+ const newSize = getSize(startSize, w, h);
+ target.style[container.isVertical ? "height" : "width"] = newSize + "px";
+ container.dispatchEvent(new CustomEvent("resizing", { detail: { oldSize: startSize, newSize }, bubbles: false }));
+ },
+ onmouseup: ({ w, h }) => {
+ const newSize = getSize(startSize, w, h);
+ container.dispatchEvent(new CustomEvent("resize", { detail: { oldSize: startSize, newSize }, bubbles: false }));
+ container.dispatchEvent(new CustomEvent("change", { detail: newSize, bubbles: false }));
+ }
+ });
+ });
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+`
+ ));
+ Object.defineProperty(exports2, "State", {
+ enumerable: true,
+ get: () => state.State
+ });
+ exports2.APIComponent = APIComponent;
+ exports2.AutoForm = AutoForm;
+ exports2.HTTP = HTTP;
+ exports2.MouseMover = MouseMover;
+ exports2.UI = UI;
+ exports2.VirtualScroll = VirtualScroll;
+ Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
+});
diff --git a/test/lib/datatable.js b/test/lib/datatable.js
new file mode 100644
index 0000000..e0bd832
--- /dev/null
+++ b/test/lib/datatable.js
@@ -0,0 +1,965 @@
+(function(factory) {
+ typeof define === "function" && define.amd ? define(factory) : factory();
+})(function() {
+ "use strict";
+ const MODE_MAP = {
+ text: ["contains", "equals", "starts", "ends"],
+ textarea: ["contains", "equals", "starts", "ends"],
+ number: ["=", ">", "<", "between"],
+ date: ["=", ">", "<", "between"],
+ select: ["contains", "equals"],
+ TagsInput: ["contains", "equals", "starts", "ends"]
+ };
+ const MODE_ICONS = {
+ "contains": "bi-search",
+ "equals": "bi-distribute-vertical",
+ "starts": "bi-align-start",
+ "ends": "bi-align-end",
+ "=": "bi-calculator",
+ ">": "bi-chevron-right",
+ "<": "bi-chevron-left",
+ "between": "bi-arrows-expand"
+ };
+ const DataTableConfig = {
+ _fieldTypes: /* @__PURE__ */ new Map(),
+ registerFieldType: (config) => {
+ DataTableConfig._fieldTypes.set(config.value, config);
+ },
+ getFieldTypes: () => Array.from(DataTableConfig._fieldTypes.values())
+ };
+ DataTableConfig.registerFieldType({
+ value: "text",
+ label: "{#Text#}",
+ typeForDB: "v4096",
+ schema: [{ name: "placeholder", label: "Placeholder", type: "text", if: 'this.data.user_type=="text"' }]
+ });
+ DataTableConfig.registerFieldType({
+ value: "number",
+ label: "{#Number#}",
+ typeForDB: "ff",
+ schema: [
+ { name: "decimals", label: "Decimals", type: "number", setting: { min: 0, max: 10 }, if: 'this.data.user_type=="number"' },
+ { name: "prefix", label: "Prefix (e.g. $)", type: "text", if: 'this.data.user_type=="number"' },
+ { name: "suffix", label: "Suffix (e.g. %)", type: "text", if: 'this.data.user_type=="number"' },
+ { name: "thousandSep", label: "Thousand Sep", type: "switch", if: 'this.data.user_type=="number"' }
+ ],
+ formatter: (val, field) => {
+ if (val == null || val === "") return "";
+ let num = Number(val);
+ if (isNaN(num)) return val;
+ const s = field.settings || {};
+ if (s.decimals !== void 0) num = num.toFixed(s.decimals);
+ let str = String(num);
+ if (s.thousandSep) {
+ const parts = str.split(".");
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ str = parts.join(".");
+ }
+ return (s.prefix || "") + str + (s.suffix || "");
+ }
+ });
+ DataTableConfig.registerFieldType({
+ value: "select",
+ label: "{#Single Select#}",
+ typeForDB: "v1024",
+ schema: [{ name: "options_str", label: "Options", type: "textarea", setting: { rows: 3, placeholder: "Label:Value per line" }, if: 'this.data.user_type=="select"' }],
+ formatter: (val, field) => {
+ var _a;
+ if (val == null || val === "") return "";
+ const opts = ((_a = field.settings) == null ? void 0 : _a.options) || [];
+ const opt = opts.find((o) => typeof o === "object" ? o.value == val : o == val);
+ return opt ? typeof opt === "object" ? opt.label : opt : val;
+ }
+ });
+ DataTableConfig.registerFieldType({
+ value: "checkbox",
+ label: "{#Multi Select#}",
+ typeForDB: "v4096",
+ schema: [{ name: "options_str", label: "Options", type: "textarea", setting: { rows: 3, placeholder: "Label:Value per line" }, if: 'this.data.user_type=="checkbox"' }],
+ formatter: (val, field) => {
+ var _a;
+ if (!Array.isArray(val)) return val == null ? "" : String(val);
+ const opts = ((_a = field.settings) == null ? void 0 : _a.options) || [];
+ return val.map((v) => {
+ const opt = opts.find((o) => typeof o === "object" ? o.value == v : o == v);
+ return opt ? typeof opt === "object" ? opt.label : opt : v;
+ }).join(", ");
+ }
+ });
+ DataTableConfig.registerFieldType({
+ value: "switch",
+ label: "{#Switch#}",
+ typeForDB: "b",
+ schema: [
+ { name: "labelOn", label: "Label On", type: "text", if: 'this.data.user_type=="switch"' },
+ { name: "labelOff", label: "Label Off", type: "text", if: 'this.data.user_type=="switch"' }
+ ],
+ formatter: (val, field) => {
+ const s = field.settings || {};
+ return val ? s.labelOn || "Yes" : s.labelOff || "No";
+ }
+ });
+ DataTableConfig.registerFieldType({
+ value: "datetime",
+ label: "{#DateTime#}",
+ typeForDB: "dt",
+ schema: [{ name: "format", label: "Format", type: "text", setting: { placeholder: "YYYY-MM-DD" }, if: 'this.data.user_type=="datetime"' }]
+ });
+ DataTableConfig.registerFieldType({
+ value: "textarea",
+ label: "{#Long Text#}",
+ typeForDB: "t",
+ schema: [{ name: "placeholder", label: "Placeholder", type: "text", if: 'this.data.user_type=="textarea"' }]
+ });
+ const createPerfMonitor = () => {
+ let enabled = !!globalThis.__DT_PERF_MODE__;
+ const stats = { refreshTime: 0, refreshCount: 0, scrollCount: 0, totalNodes: 0 };
+ if (enabled && !globalThis.__statePerformanceTelemetry) {
+ globalThis.__statePerformanceTelemetry = { scanCount: 0, reuseCount: 0, moveCount: 0 };
+ }
+ return {
+ get stats() {
+ return stats;
+ },
+ enable: () => {
+ enabled = true;
+ },
+ disable: () => {
+ enabled = false;
+ },
+ onScroll: () => {
+ if (enabled) stats.scrollCount++;
+ },
+ startFrame: () => {
+ var _a, _b, _c;
+ if (!enabled) return null;
+ return {
+ start: performance.now(),
+ scan: ((_a = globalThis.__statePerformanceTelemetry) == null ? void 0 : _a.scanCount) || 0,
+ move: ((_b = globalThis.__statePerformanceTelemetry) == null ? void 0 : _b.moveCount) || 0,
+ reuse: ((_c = globalThis.__statePerformanceTelemetry) == null ? void 0 : _c.reuseCount) || 0
+ };
+ },
+ endFrame: (startData, renderedCount) => {
+ if (!enabled || !startData) return;
+ stats.refreshCount++;
+ stats.totalNodes += renderedCount;
+ const elapsed = performance.now() - startData.start;
+ stats.refreshTime += elapsed;
+ const stPerf = globalThis.__statePerformanceTelemetry;
+ if (stPerf) {
+ const scans = stPerf.scanCount - startData.scan;
+ const moves = stPerf.moveCount - startData.move;
+ const reuses = stPerf.reuseCount - startData.reuse;
+ if (scans > 0 || elapsed > 2) {
+ console.log(`[DataTable Frame] Time: ${elapsed.toFixed(2)}ms, Scans: ${scans}, Moves: ${moves}, Reuses: ${reuses}, Rows: ${renderedCount}`);
+ }
+ }
+ }
+ };
+ };
+ const createScrollManager = (container, state, onRenderedListChange) => {
+ const vs = globalThis.VirtualScroll({ itemHeight: 40 });
+ let scrollEl = null;
+ const refresh = (isLayoutChange = false) => {
+ if (!scrollEl) return;
+ const res = vs.calc(scrollEl, state.list);
+ if (res) {
+ if (!isLayoutChange && state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) return;
+ Object.assign(state, { prevHeight: res.prevHeight, postHeight: res.postHeight, _listStartIndex: res.listStartIndex, _renderedList: res.renderedList });
+ onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length, isLayoutChange);
+ }
+ };
+ return {
+ init: () => {
+ scrollEl = container.querySelector(".dt-main");
+ },
+ reset: (list) => {
+ state._listStartIndex = 0;
+ vs.reset(list, scrollEl || container);
+ if (state.list === list) vs.init(list, () => refresh(true));
+ },
+ refresh,
+ onScroll: () => refresh(false)
+ };
+ };
+ const createSelectionManager = (container, state) => {
+ let activeBounds = null;
+ let startCell = null;
+ let multiSelections = [];
+ const isCellSelected = (r, c) => {
+ if (activeBounds && r >= activeBounds.minRow && r <= activeBounds.maxRow && c >= activeBounds.minCol && c <= activeBounds.maxCol) return true;
+ return multiSelections.some((s) => r >= s.minRow && r <= s.maxRow && c >= s.minCol && c <= s.maxCol);
+ };
+ let lastHadSelection = false;
+ const applySelectionUI = () => {
+ if (globalThis.__DT_FEATURES__ && !globalThis.__DT_FEATURES__.selection) return;
+ let boundMinRow = Infinity, boundMaxRow = -Infinity;
+ if (activeBounds) {
+ boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
+ boundMaxRow = Math.max(boundMaxRow, activeBounds.maxRow);
+ }
+ multiSelections.forEach((s) => {
+ boundMinRow = Math.min(boundMinRow, s.minRow);
+ boundMaxRow = Math.max(boundMaxRow, s.maxRow);
+ });
+ const hasSelection = boundMinRow !== Infinity;
+ if (!hasSelection && !lastHadSelection) return;
+ lastHadSelection = hasSelection;
+ const body = container.querySelector(".dt-body");
+ if (!body) return;
+ const rowNodes = body.querySelectorAll(".dt-body-row");
+ rowNodes.forEach((rowNode) => {
+ var _a;
+ const absoluteRow = (((_a = rowNode._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex;
+ const cells = rowNode.querySelectorAll(".dt-cell");
+ if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
+ cells.forEach((cell) => cell.classList.remove("dt-cell-selected"));
+ return;
+ }
+ cells.forEach((cell, cIdx) => {
+ if (isCellSelected(absoluteRow, cIdx)) cell.classList.add("dt-cell-selected");
+ else cell.classList.remove("dt-cell-selected");
+ });
+ });
+ };
+ const updateStatus = () => {
+ let count = 0;
+ if (activeBounds) count += activeBounds.maxRow - activeBounds.minRow + 1;
+ multiSelections.forEach((s) => count += s.maxRow - s.minRow + 1);
+ state.selectedRowCount = count;
+ };
+ const clearAllActive = (keepSelection = false) => {
+ if (!keepSelection) {
+ activeBounds = null;
+ startCell = null;
+ multiSelections = [];
+ applySelectionUI();
+ updateStatus();
+ }
+ };
+ const startSelect = (row, col, e) => {
+ const alreadySelected = isCellSelected(row, col);
+ const isRange = activeBounds && (activeBounds.minRow !== activeBounds.maxRow || activeBounds.minCol !== activeBounds.maxCol) || multiSelections.length > 0;
+ if (e.shiftKey && startCell) {
+ activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) };
+ } else {
+ if (alreadySelected && !e.ctrlKey && !e.metaKey) {
+ if (!isRange) container._potentialCancel = { row, col };
+ } else {
+ if (!e.ctrlKey && !e.metaKey) clearAllActive();
+ else if (activeBounds && !alreadySelected) multiSelections.push(activeBounds);
+ startCell = { row, col };
+ activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col };
+ }
+ state.isSelecting = true;
+ }
+ applySelectionUI();
+ updateStatus();
+ container.focus();
+ };
+ const updateSelect = (row, col) => {
+ if (state.isSelecting && startCell) {
+ activeBounds = { minRow: Math.min(startCell.row, row), maxRow: Math.max(startCell.row, row), minCol: Math.min(startCell.col, col), maxCol: Math.max(startCell.col, col) };
+ container._potentialCancel = null;
+ applySelectionUI();
+ updateStatus();
+ }
+ };
+ const endSelect = () => {
+ if (container._potentialCancel) {
+ const { row, col } = container._potentialCancel;
+ if (isCellSelected(row, col)) clearAllActive();
+ container._potentialCancel = null;
+ }
+ state.isSelecting = false;
+ };
+ const getSelectionBounds = () => {
+ if (!activeBounds) return null;
+ let minRow = activeBounds.minRow, maxRow = activeBounds.maxRow, minCol = activeBounds.minCol, maxCol = activeBounds.maxCol;
+ multiSelections.forEach((s) => {
+ minRow = Math.min(minRow, s.minRow);
+ maxRow = Math.max(maxRow, s.maxRow);
+ minCol = Math.min(minCol, s.minCol);
+ maxCol = Math.max(maxCol, s.maxCol);
+ });
+ return { minRow, maxRow, minCol, maxCol };
+ };
+ const copy = async () => {
+ const bounds = getSelectionBounds();
+ if (!bounds) return;
+ const text = state.list.slice(bounds.minRow, bounds.maxRow + 1).map((row) => {
+ return state.fields.slice(bounds.minCol, bounds.maxCol + 1).map((f) => {
+ let val = String(row[f.id] ?? "");
+ if (val.includes(" ") || val.includes("\n") || val.includes('"')) val = '"' + val.replace(/"/g, '""') + '"';
+ return val;
+ }).join(" ");
+ }).join("\n");
+ await navigator.clipboard.writeText(text);
+ };
+ const paste = async () => {
+ try {
+ const text = await navigator.clipboard.readText();
+ if (!text) return;
+ const bounds = getSelectionBounds();
+ if (!bounds) return;
+ const rows = text.split(/\r?\n/).filter((line) => line.length > 0).map((line) => {
+ const cells = [];
+ let current = "", inQuotes = false;
+ for (let i = 0; i < line.length; i++) {
+ const char = line[i];
+ if (char === '"') {
+ if (inQuotes && line[i + 1] === '"') {
+ current += '"';
+ i++;
+ } else inQuotes = !inQuotes;
+ } else if (char === " " && !inQuotes) {
+ cells.push(current);
+ current = "";
+ } else current += char;
+ }
+ cells.push(current);
+ return cells;
+ });
+ const { minRow: startRow, minCol: startCol, maxRow, maxCol } = bounds;
+ const body = container.querySelector(".dt-body");
+ const rowNodes = body ? Array.from(body.childNodes).filter((n) => {
+ var _a;
+ return (_a = n.classList) == null ? void 0 : _a.contains("dt-body-row");
+ }) : [];
+ let anyRowChanged = false;
+ rows.forEach((rowData, rOffset) => {
+ const rIdx = startRow + rOffset;
+ if (rIdx > maxRow || rIdx >= state.list.length) return;
+ const rowItem = state.list[rIdx];
+ let rowChanged = false;
+ rowData.forEach((cellData, cOffset) => {
+ const cIdx = startCol + cOffset;
+ if (cIdx > maxCol || cIdx >= state.fields.length) return;
+ const field = state.fields[cIdx];
+ rowItem[field.id] = cellData;
+ rowChanged = true;
+ });
+ if (rowChanged) anyRowChanged = true;
+ });
+ if (anyRowChanged) state.list = [...state.list];
+ } catch (err) {
+ console.error("Paste Error:", err);
+ }
+ };
+ return { applySelectionUI, clearAllActive, startSelect, updateSelect, endSelect, getSelectionBounds, copy, paste };
+ };
+ globalThis.Component.register("DataTable", (container) => {
+ if (!container.state) container.state = globalThis.NewState({});
+ const state = container.state;
+ Object.assign(state, {
+ list: [],
+ fields: [],
+ _renderedList: [],
+ prevHeight: 0,
+ postHeight: 0,
+ _listStartIndex: 0,
+ selectedRowCount: 0,
+ _originalList: [],
+ sortConfig: { fieldId: null, direction: null },
+ filterConfig: {},
+ activeFieldId: null,
+ activeField: null,
+ activeModes: [],
+ _columnStats: {},
+ _internalUpdate: false,
+ _appliedHash: "",
+ _fieldsDirty: false,
+ _masterCellNodes: null,
+ isDirty: false,
+ isBulkEdit: null
+ });
+ const perf = createPerfMonitor();
+ state.perf = perf.stats;
+ const selection = createSelectionManager(container, state);
+ const scroll = createScrollManager(container, state, () => selection.applySelectionUI());
+ const menuNode = container.querySelector(".dt-column-menu");
+ if (menuNode) menuNode._thisObj = container;
+ container.onColumnResizing = (field, e) => container.style.setProperty(`--w-${field.id}`, e.detail.newSize + "px");
+ container.onColumnResize = (field, e) => {
+ const idx = state.fields.findIndex((f) => f.id === field.id);
+ if (idx !== -1) {
+ state.fields[idx].width = e.detail.newSize;
+ state.fields = [...state.fields];
+ }
+ };
+ let _editorOverlay, currentEditingNode = null;
+ container.format = (val, field) => {
+ var _a;
+ if (field.formatter) return field.formatter(val, field);
+ const typeInfo = DataTableConfig._fieldTypes.get(((_a = field.settings) == null ? void 0 : _a.formType) || field.type || "text");
+ if (typeInfo && typeInfo.formatter) return typeInfo.formatter(val, field);
+ return val == null ? "" : typeof val === "object" ? JSON.stringify(val) : String(val);
+ };
+ container.onScroll = () => {
+ perf.onScroll();
+ scroll.refresh();
+ container.hideColumnMenu();
+ const prev = container.querySelector(".dt-spacer-prev"), post = container.querySelector(".dt-spacer-post");
+ if (prev) {
+ prev.style.height = (state.prevHeight || 0) + "px";
+ prev.style.display = state.prevHeight > 0 ? "block" : "none";
+ }
+ if (post) {
+ post.style.height = (state.postHeight || 0) + "px";
+ post.style.display = state.postHeight > 0 ? "block" : "none";
+ }
+ };
+ container.applySortFilter = (options = {}) => {
+ if (state._internalUpdate) return;
+ const targetFilters = { ...state.filterConfig, ...options.filters || {} };
+ const targetSort = options.sort !== void 0 ? options.sort ? { fieldId: state.activeFieldId, direction: options.sort } : { fieldId: null, direction: null } : state.sortConfig;
+ let filtered = [...state._originalList];
+ Object.entries(targetFilters).forEach(([fId, cfg]) => {
+ if (!cfg.value && (!cfg.selectedValues || cfg.selectedValues.length === 0)) return;
+ filtered = filtered.filter((item) => {
+ var _a;
+ const val = item[fId];
+ if (((_a = cfg.selectedValues) == null ? void 0 : _a.length) > 0) return cfg.selectedValues.includes(String(val));
+ const search = String(cfg.value).toLowerCase();
+ const target = String(val ?? "").toLowerCase();
+ switch (cfg.mode) {
+ case "contains":
+ return target.includes(search);
+ case "equals":
+ return target === search;
+ case "starts":
+ return target.startsWith(search);
+ case "ends":
+ return target.endsWith(search);
+ case "=":
+ return Number(val) === Number(cfg.value);
+ case ">":
+ return Number(val) > Number(cfg.value);
+ case "<":
+ return Number(val) < Number(cfg.value);
+ case "between":
+ return Number(val) >= Number(cfg.value) && Number(val) <= Number(cfg.value2);
+ default:
+ return true;
+ }
+ });
+ });
+ if (targetSort.fieldId && targetSort.direction) {
+ const fId = targetSort.fieldId;
+ const dir = targetSort.direction === "asc" ? 1 : -1;
+ filtered.sort((a, b) => {
+ if (a[fId] == b[fId]) return 0;
+ return a[fId] > b[fId] ? dir : -dir;
+ });
+ }
+ state._internalUpdate = true;
+ state.filterConfig = targetFilters;
+ state.sortConfig = targetSort;
+ state.list = filtered;
+ state._internalUpdate = false;
+ };
+ container.showColumnMenu = (field, event) => {
+ var _a;
+ const btn = event.currentTarget, menu = container.querySelector(".dt-column-menu");
+ const type = ((_a = field.settings) == null ? void 0 : _a.formType) || field.type || "text";
+ state.activeModes = MODE_MAP[type] || (["boolean", "switch", "checkbox", "radio"].includes(type) ? [] : MODE_MAP.text);
+ if (!state.filterConfig[field.id]) state.filterConfig[field.id] = { mode: state.activeModes[0] || "contains", value: "", selectedValues: [] };
+ state.activeField = field;
+ state.activeFieldId = field.id;
+ menu.style.display = "block";
+ const cellNode = btn.closest(".dt-cell"), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
+ const menuWidth = menu.offsetWidth || 260;
+ let leftPos = rect.right - rootRect.left - menuWidth;
+ if (leftPos < 0) leftPos = Math.max(0, rect.left - rootRect.left);
+ menu.style.left = leftPos + "px";
+ menu.style.top = rect.bottom - rootRect.top + 5 + "px";
+ const onGlobalClick = (ev) => {
+ if (menu.contains(ev.target) || btn.contains(ev.target)) return;
+ container.hideColumnMenu();
+ container.applySortFilter();
+ document.removeEventListener("mousedown", onGlobalClick);
+ };
+ document.addEventListener("mousedown", onGlobalClick);
+ setTimeout(() => {
+ var _a2;
+ return (_a2 = menu.querySelector("input")) == null ? void 0 : _a2.focus();
+ }, 50);
+ };
+ container.toggleSelectedValue = (val) => {
+ const filter = state.filterConfig[state.activeFieldId];
+ if (!filter) return;
+ const idx = filter.selectedValues.indexOf(val);
+ if (idx === -1) filter.selectedValues.push(val);
+ else filter.selectedValues.splice(idx, 1);
+ state.filterConfig = { ...state.filterConfig };
+ container.applySortFilter();
+ };
+ container.filterOnlyThis = (val) => {
+ state.filterConfig[state.activeFieldId] = { mode: "contains", value: "", selectedValues: [String(val)] };
+ state.filterConfig = { ...state.filterConfig };
+ container.applySortFilter();
+ };
+ container.hideColumnMenu = () => {
+ const menu = container.querySelector(".dt-column-menu");
+ if (menu) menu.style.display = "none";
+ };
+ container.setSort = (dir) => {
+ const newDir = state.sortConfig.direction === dir && state.sortConfig.fieldId === state.activeFieldId ? null : dir;
+ container.applySortFilter({ sort: newDir });
+ };
+ container.clearColumnSettings = () => {
+ if (state.activeFieldId) {
+ delete state.filterConfig[state.activeFieldId];
+ state.filterConfig = { ...state.filterConfig };
+ container.applySortFilter();
+ }
+ };
+ container._initRow = (rowNode) => {
+ var _a;
+ const row = (_a = rowNode._ref) == null ? void 0 : _a.item;
+ if (row && row._editingF === void 0) {
+ Object.defineProperty(row, "_editingF", { set: (v) => {
+ if (v === null) container.hideEditor(true);
+ }, configurable: true });
+ }
+ Array.from(rowNode.children).forEach((cell) => {
+ const fIdx = parseInt(cell.dataset.fidx);
+ if (!isNaN(fIdx)) cell._ref = { ...cell._ref || rowNode._ref, f: state.fields[fIdx], fIdx };
+ });
+ };
+ state.__watch("fields", (fields) => {
+ if (!fields) return;
+ state._fieldsDirty = true;
+ state._masterCellNodes = null;
+ container.style.setProperty("--dt-grid-template", fields.map((f) => {
+ var _a;
+ return `var(--w-${f.id}, ${((_a = f.settings) == null ? void 0 : _a.width) || f.width || 150}px)`;
+ }).join(" "));
+ container.style.setProperty("--dt-row-width", fields.reduce((sum, f) => {
+ var _a;
+ return sum + (((_a = f.settings) == null ? void 0 : _a.width) || f.width || 150);
+ }, 0) + "px");
+ let leftSum = 0;
+ fields.forEach((f) => {
+ var _a, _b;
+ const pinned = ((_a = f.settings) == null ? void 0 : _a.pinned) || f.pinned;
+ if (pinned === "left") {
+ container.style.setProperty(`--l-${f.id}`, leftSum + "px");
+ leftSum += ((_b = f.settings) == null ? void 0 : _b.width) || f.width || 150;
+ }
+ });
+ let rightSum = 0;
+ [...fields].reverse().forEach((f) => {
+ var _a, _b;
+ const pinned = ((_a = f.settings) == null ? void 0 : _a.pinned) || f.pinned;
+ if (pinned === "right") {
+ container.style.setProperty(`--r-${f.id}`, rightSum + "px");
+ rightSum += ((_b = f.settings) == null ? void 0 : _b.width) || f.width || 150;
+ }
+ });
+ });
+ state.__watch("list", (list) => {
+ var _a;
+ if (state._fieldsDirty) {
+ state._fieldsDirty = false;
+ const fieldTemplate = (_a = container.querySelector('.dt-body template[index="rIdx"]')) == null ? void 0 : _a.content.querySelector('template[as="f"]');
+ if (fieldTemplate) {
+ const masters = state._masterCellNodes || (state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map((n) => n.cloneNode(true)));
+ fieldTemplate.removeAttribute("$each");
+ fieldTemplate.setAttribute("$if", "true");
+ fieldTemplate.content.textContent = "";
+ state.fields.forEach((f, fIdx) => masters.forEach((master) => {
+ var _a2;
+ const clone = master.cloneNode(true);
+ if (clone.nodeType === 1) {
+ clone.dataset.fidx = fIdx;
+ const pinned = ((_a2 = f.settings) == null ? void 0 : _a2.pinned) || f.pinned;
+ if (pinned) {
+ clone.classList.add("pinned-" + pinned);
+ clone.style.position = "sticky";
+ clone.style.zIndex = "1";
+ clone.style.backgroundColor = "inherit";
+ if (pinned === "left") {
+ clone.style.left = `var(--l-${f.id})`;
+ clone.style.borderRight = "1px solid var(--bs-border-color)";
+ clone.style.boxShadow = "2px 0 5px -2px rgba(0,0,0,0.1)";
+ } else {
+ clone.style.right = `var(--r-${f.id})`;
+ clone.style.borderLeft = "1px solid var(--bs-border-color)";
+ clone.style.boxShadow = "-2px 0 5px -2px rgba(0,0,0,0.1)";
+ }
+ }
+ }
+ fieldTemplate.content.appendChild(clone);
+ }));
+ }
+ }
+ if (!state._internalUpdate) {
+ state._originalList = [...list || []];
+ setTimeout(() => {
+ const stats = {};
+ state.fields.forEach((f) => {
+ const counts = {};
+ state._originalList.forEach((item) => {
+ const val = item[f.id], key = val == null || val === "" ? "" : String(val);
+ counts[key] = (counts[key] || 0) + 1;
+ });
+ stats[f.id] = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 20).map(([val, count]) => ({ val, count }));
+ });
+ state._columnStats = stats;
+ }, 200);
+ }
+ scroll.init();
+ scroll.reset(list);
+ });
+ container.editCell = (row, field, cellNode) => {
+ var _a, _b;
+ const overlay = container.querySelector(".dt-editor-overlay"), rect = cellNode.getBoundingClientRect(), rootRect = container.getBoundingClientRect();
+ currentEditingNode = cellNode;
+ const formType = ((_a = field.settings) == null ? void 0 : _a.formType) || field.type || "text";
+ const form = overlay.querySelector("AutoForm");
+ if (form) {
+ form.data = row;
+ form.state.schema = [{ ...field, type: formType, options: ((_b = field.settings) == null ? void 0 : _b.options) || field.options, name: field.id, label: "" }];
+ }
+ Object.assign(overlay.style, {
+ display: "flex",
+ left: rect.left - rootRect.left + "px",
+ top: rect.top - rootRect.top + "px",
+ width: (formType === "textarea" || formType === "TagsInput" ? Math.max(rect.width, 300) : rect.width) + "px",
+ height: formType === "textarea" || formType === "TagsInput" ? "auto" : rect.height + "px"
+ });
+ setTimeout(() => {
+ var _a2;
+ return (_a2 = overlay.querySelector("input, textarea, select, .form-control")) == null ? void 0 : _a2.focus();
+ }, 30);
+ };
+ container.hideEditor = (save = true) => {
+ if (!_editorOverlay) _editorOverlay = container.querySelector(".dt-editor-overlay");
+ if (!_editorOverlay || _editorOverlay.style.display === "none") return;
+ const form = _editorOverlay.querySelector("AutoForm");
+ if (save && form && form.data) {
+ const input = _editorOverlay.querySelector("input:focus, select:focus, textarea:focus");
+ if (input) input.dispatchEvent(new Event(input.type === "number" || input.tagName === "SELECT" ? "change" : "input", { bubbles: true }));
+ const schema = form.state.schema || [];
+ schema.forEach((field) => {
+ var _a, _b;
+ const row = (_b = (_a = currentEditingNode == null ? void 0 : currentEditingNode.closest(".dt-row")) == null ? void 0 : _a._ref) == null ? void 0 : _b.item;
+ if (row) row[field.name] = form.data[field.name];
+ });
+ if (state.isBulkEdit) {
+ const { minRow, maxRow, fIdx } = state.isBulkEdit;
+ const field = state.fields[fIdx];
+ const newValue = form.data[field.id];
+ for (let i = minRow; i <= maxRow; i++) {
+ if (state.list[i]) state.list[i][field.id] = newValue;
+ }
+ }
+ state.list = [...state.list];
+ state.isDirty = true;
+ }
+ _editorOverlay.style.display = "none";
+ if (form) {
+ form.state.schema = [];
+ form.data = null;
+ }
+ currentEditingNode = null;
+ state.isBulkEdit = null;
+ container.focus();
+ };
+ container.onMainMouseDown = (e) => {
+ var _a;
+ const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
+ if (!row || row.classList.contains("dt-header-row")) return;
+ const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
+ const rIdx = ((_a = row._ref) == null ? void 0 : _a.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
+ selection.startSelect(rIdx + state._listStartIndex, fIdx, e);
+ };
+ container.onMainMouseOver = (e) => {
+ var _a;
+ if (state.isSelecting) {
+ const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
+ if (row && !row.classList.contains("dt-header-row")) {
+ const fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
+ const rIdx = ((_a = row._ref) == null ? void 0 : _a.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
+ selection.updateSelect(rIdx + state._listStartIndex, fIdx);
+ }
+ }
+ };
+ container.onMainDblClick = (e) => {
+ var _a, _b, _c;
+ const cell = e.target.closest(".dt-cell"), row = cell == null ? void 0 : cell.closest(".dt-row");
+ if (row && !row.classList.contains("dt-header-row")) {
+ const item = (_a = row._ref) == null ? void 0 : _a.item, fIdx = cell.dataset.fidx ? parseInt(cell.dataset.fidx) : Array.from(row.querySelectorAll(".dt-cell")).indexOf(cell);
+ const rIdx = ((_b = row._ref) == null ? void 0 : _b.rIdx) ?? Array.from(container.querySelectorAll(".dt-body-row")).indexOf(row);
+ const absoluteRow = rIdx + state._listStartIndex;
+ if (item && state.fields[fIdx]) {
+ const bounds = selection.getSelectionBounds();
+ if (bounds && absoluteRow >= bounds.minRow && absoluteRow <= bounds.maxRow && fIdx >= bounds.minCol && fIdx <= bounds.maxCol) {
+ const affectedRows = bounds.maxRow - bounds.minRow + 1;
+ if (affectedRows > 1) {
+ state.isBulkEdit = { ...bounds, fIdx };
+ if ((_c = globalThis.UI) == null ? void 0 : _c.toast) globalThis.UI.toast(`Bulk Edit: Updating ${affectedRows} rows in column "${state.fields[fIdx].name}"`, { type: "warning" });
+ }
+ }
+ container.editCell(item, state.fields[fIdx], cell);
+ }
+ }
+ };
+ container.addRow = () => {
+ const newRow = {};
+ state.fields.forEach((f) => newRow[f.id] = "");
+ state._originalList.push(newRow);
+ state.list = [...state._originalList];
+ state.isDirty = true;
+ setTimeout(() => {
+ scroll.reset(state.list);
+ container.querySelector(".dt-main").scrollTop = container.querySelector(".dt-main").scrollHeight;
+ }, 50);
+ };
+ container.deleteSelectedRow = async () => {
+ const bounds = selection.getSelectionBounds();
+ if (!bounds) return;
+ const count = bounds.maxRow - bounds.minRow + 1;
+ if (await globalThis.UI.confirm(`Are you sure you want to delete ${count} row(s)?`)) {
+ const rMin = bounds.minRow, rMax = bounds.maxRow;
+ const removedItems = state.list.slice(rMin, rMax + 1);
+ state.list = state.list.filter((_, i) => !(i >= rMin && i <= rMax));
+ state._originalList = state._originalList.filter((item) => !removedItems.includes(item));
+ state.isDirty = true;
+ selection.clearAllActive();
+ container.dispatchEvent(new CustomEvent("remove", { detail: { items: removedItems } }));
+ }
+ };
+ container.saveChanges = () => {
+ container.dispatchEvent(new CustomEvent("save", { detail: { list: state._originalList, fields: state.fields } }));
+ state.isDirty = false;
+ };
+ const getFieldSchema = () => {
+ const types = globalThis.DataTable.getFieldTypes();
+ const baseSchema = [
+ { name: "id", label: "Field ID", type: "text", setting: { required: true, placeholder: "e.g. user_name" } },
+ { name: "name", label: "Display Name", type: "text", setting: { required: true, placeholder: "e.g. 用户名" } },
+ { name: "user_type", label: "Field Type", type: "select", options: types.map((t) => ({ label: t.label, value: t.value })) }
+ ];
+ const dynamicSchema = types.reduce((acc, t) => acc.concat(t.schema || []), []);
+ return baseSchema.concat(dynamicSchema, [{ name: "isIndex", label: "Index", type: "switch" }, { name: "memo", label: "Memo", type: "text" }]);
+ };
+ const parseOptionsStr = (str) => {
+ if (!str) return void 0;
+ return str.split("\n").map((s) => s.trim()).filter(Boolean).map((line) => {
+ const idx = line.indexOf(":");
+ if (idx > -1) return { label: line.slice(0, idx).trim(), value: line.slice(idx + 1).trim() };
+ return line;
+ });
+ };
+ const formatOptionsStr = (opts) => {
+ if (!opts) return "";
+ return opts.map((o) => typeof o === "object" ? `${o.label}:${o.value}` : o).join("\n");
+ };
+ container.addField = async () => {
+ container.hideColumnMenu();
+ const data = globalThis.NewState({ id: "c" + Date.now().toString().slice(-4), name: "New Field", user_type: "text", decimals: 0, isIndex: false, memo: "", options_str: "" });
+ const d = container.querySelector(`Dialog[id="${container.id}_field_dialog"]`);
+ if (!d) return;
+ Object.assign(d.state, { title: "Add Field", buttons: ["Cancel", "Save"] });
+ const form = d.querySelector("AutoForm");
+ if (form) {
+ form.data = data;
+ form.state.schema = getFieldSchema();
+ }
+ d.show();
+ const result = await new Promise((resolve) => d.addEventListener("change", (e) => resolve(d.result), { once: true }));
+ if (result === 2) {
+ const typeInfo = globalThis.DataTable.getFieldTypes().find((t) => t.value === data.user_type);
+ let dbType = (typeInfo == null ? void 0 : typeInfo.typeForDB) || "v1024";
+ if (data.user_type === "number") dbType = data.decimals > 0 ? "ff" : "bi";
+ const field = { id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } };
+ state.fields = [...state.fields, field];
+ state.isDirty = true;
+ container.dispatchEvent(new CustomEvent("savefields", { detail: state.fields }));
+ state.list = [...state.list];
+ }
+ };
+ container.editField = async () => {
+ if (!state.activeField) return;
+ container.hideColumnMenu();
+ const f = state.activeField;
+ const s = f.settings || {};
+ const data = globalThis.NewState({ id: f.id, name: f.name, memo: f.memo || "", isIndex: !!f.isIndex, user_type: s.formType || "text", decimals: s.decimals || 0, prefix: s.prefix || "", suffix: s.suffix || "", thousandSep: !!s.thousandSep, labelOn: s.labelOn || "", labelOff: s.labelOff || "", format: s.format || "", placeholder: s.placeholder || "", options_str: formatOptionsStr(s.options) });
+ const d = container.querySelector(`Dialog[id="${container.id}_field_dialog"]`);
+ if (!d) return;
+ Object.assign(d.state, { title: "Edit Field", buttons: ["Cancel", "Save"] });
+ const form = d.querySelector("AutoForm");
+ if (form) {
+ form.data = data;
+ form.state.schema = getFieldSchema();
+ }
+ d.show();
+ const result = await new Promise((resolve) => d.addEventListener("change", (e) => resolve(d.result), { once: true }));
+ if (result === 2) {
+ const idx = state.fields.findIndex((item) => item.id === f.id);
+ if (idx !== -1) {
+ const typeInfo = globalThis.DataTable.getFieldTypes().find((t) => t.value === data.user_type);
+ let dbType = (typeInfo == null ? void 0 : typeInfo.typeForDB) || "v1024";
+ if (data.user_type === "number") dbType = data.decimals > 0 ? "ff" : "bi";
+ const updatedField = { ...f, id: data.id, name: data.name, memo: data.memo, isIndex: !!data.isIndex, type: dbType, settings: { ...f.settings, formType: data.user_type, decimals: data.decimals, prefix: data.prefix, suffix: data.suffix, thousandSep: data.thousandSep, labelOn: data.labelOn, labelOff: data.labelOff, format: data.format, placeholder: data.placeholder, options: parseOptionsStr(data.options_str) } };
+ state.fields[idx] = updatedField;
+ state.fields = [...state.fields];
+ state.isDirty = true;
+ container.dispatchEvent(new CustomEvent("savefields", { detail: state.fields }));
+ state.list = [...state.list];
+ }
+ }
+ };
+ container.deleteField = async () => {
+ if (!state.activeField) return;
+ container.hideColumnMenu();
+ if (await globalThis.UI.confirm(`Are you sure you want to delete field "${state.activeField.name}"?`)) {
+ const idx = state.fields.findIndex((f) => f.id === state.activeField.id);
+ if (idx !== -1) {
+ state.fields.splice(idx, 1);
+ state.fields = [...state.fields];
+ state.isDirty = true;
+ container.dispatchEvent(new CustomEvent("savefields", { detail: state.fields }));
+ state.list = [...state.list];
+ }
+ }
+ };
+ window.addEventListener("mouseup", selection.endSelect);
+ document.addEventListener("mousedown", (e) => {
+ const overlay = container.querySelector(".dt-editor-overlay");
+ const menu = container.querySelector(".dt-column-menu");
+ if ((overlay == null ? void 0 : overlay.style.display) !== "none" && !overlay.contains(e.target)) container.hideEditor(true);
+ if (!container.contains(e.target) && !(overlay == null ? void 0 : overlay.contains(e.target)) && !(menu == null ? void 0 : menu.contains(e.target))) selection.clearAllActive();
+ });
+ state._MODE_ICONS = MODE_ICONS;
+ }, globalThis.Util.makeDom(
+ /*html*/
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TOP FREQUENT VALUES
+
+
+
+
+
+
+ Clear Filter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ ), globalThis.Util.makeDom(
+ /*html*/
+ `
+
+`
+ ));
+ globalThis.DataTable = DataTableConfig;
+});
diff --git a/test/lib/datatable.min.js b/test/lib/datatable.min.js
new file mode 100644
index 0000000..3e0cb5e
--- /dev/null
+++ b/test/lib/datatable.min.js
@@ -0,0 +1 @@
+!function(e){"function"==typeof define&&define.amd?define(e):e()}(function(){"use strict";const e={text:["contains","equals","starts","ends"],textarea:["contains","equals","starts","ends"],number:["=",">","<","between"],date:["=",">","<","between"],select:["contains","equals"],TagsInput:["contains","equals","starts","ends"]},t={contains:"bi-search",equals:"bi-distribute-vertical",starts:"bi-align-start",ends:"bi-align-end","=":"bi-calculator",">":"bi-chevron-right","<":"bi-chevron-left",between:"bi-arrows-expand"},i={_fieldTypes:new Map,registerFieldType:e=>{i._fieldTypes.set(e.value,e)},getFieldTypes:()=>Array.from(i._fieldTypes.values())};i.registerFieldType({value:"text",label:"{#Text#}",typeForDB:"v4096",schema:[{name:"placeholder",label:"Placeholder",type:"text",if:'this.data.user_type=="text"'}]}),i.registerFieldType({value:"number",label:"{#Number#}",typeForDB:"ff",schema:[{name:"decimals",label:"Decimals",type:"number",setting:{min:0,max:10},if:'this.data.user_type=="number"'},{name:"prefix",label:"Prefix (e.g. $)",type:"text",if:'this.data.user_type=="number"'},{name:"suffix",label:"Suffix (e.g. %)",type:"text",if:'this.data.user_type=="number"'},{name:"thousandSep",label:"Thousand Sep",type:"switch",if:'this.data.user_type=="number"'}],formatter:(e,t)=>{if(null==e||""===e)return"";let i=Number(e);if(isNaN(i))return e;const n=t.settings||{};void 0!==n.decimals&&(i=i.toFixed(n.decimals));let l=String(i);if(n.thousandSep){const e=l.split(".");e[0]=e[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),l=e.join(".")}return(n.prefix||"")+l+(n.suffix||"")}}),i.registerFieldType({value:"select",label:"{#Single Select#}",typeForDB:"v1024",schema:[{name:"options_str",label:"Options",type:"textarea",setting:{rows:3,placeholder:"Label:Value per line"},if:'this.data.user_type=="select"'}],formatter:(e,t)=>{var i;if(null==e||""===e)return"";const n=((null==(i=t.settings)?void 0:i.options)||[]).find(t=>"object"==typeof t?t.value==e:t==e);return n?"object"==typeof n?n.label:n:e}}),i.registerFieldType({value:"checkbox",label:"{#Multi Select#}",typeForDB:"v4096",schema:[{name:"options_str",label:"Options",type:"textarea",setting:{rows:3,placeholder:"Label:Value per line"},if:'this.data.user_type=="checkbox"'}],formatter:(e,t)=>{var i;if(!Array.isArray(e))return null==e?"":String(e);const n=(null==(i=t.settings)?void 0:i.options)||[];return e.map(e=>{const t=n.find(t=>"object"==typeof t?t.value==e:t==e);return t?"object"==typeof t?t.label:t:e}).join(", ")}}),i.registerFieldType({value:"switch",label:"{#Switch#}",typeForDB:"b",schema:[{name:"labelOn",label:"Label On",type:"text",if:'this.data.user_type=="switch"'},{name:"labelOff",label:"Label Off",type:"text",if:'this.data.user_type=="switch"'}],formatter:(e,t)=>{const i=t.settings||{};return e?i.labelOn||"Yes":i.labelOff||"No"}}),i.registerFieldType({value:"datetime",label:"{#DateTime#}",typeForDB:"dt",schema:[{name:"format",label:"Format",type:"text",setting:{placeholder:"YYYY-MM-DD"},if:'this.data.user_type=="datetime"'}]}),i.registerFieldType({value:"textarea",label:"{#Long Text#}",typeForDB:"t",schema:[{name:"placeholder",label:"Placeholder",type:"text",if:'this.data.user_type=="textarea"'}]});globalThis.Component.register("DataTable",n=>{n.state||(n.state=globalThis.NewState({}));const l=n.state;Object.assign(l,{list:[],fields:[],_renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selectedRowCount:0,_originalList:[],sortConfig:{fieldId:null,direction:null},filterConfig:{},activeFieldId:null,activeField:null,activeModes:[],_columnStats:{},_internalUpdate:!1,_appliedHash:"",_fieldsDirty:!1,_masterCellNodes:null,isDirty:!1,isBulkEdit:null});const s=(()=>{let e=!!globalThis.__DT_PERF_MODE__;const t={refreshTime:0,refreshCount:0,scrollCount:0,totalNodes:0};return e&&!globalThis.__statePerformanceTelemetry&&(globalThis.__statePerformanceTelemetry={scanCount:0,reuseCount:0,moveCount:0}),{get stats(){return t},enable:()=>{e=!0},disable:()=>{e=!1},onScroll:()=>{e&&t.scrollCount++},startFrame:()=>{var t,i,n;return e?{start:performance.now(),scan:(null==(t=globalThis.__statePerformanceTelemetry)?void 0:t.scanCount)||0,move:(null==(i=globalThis.__statePerformanceTelemetry)?void 0:i.moveCount)||0,reuse:(null==(n=globalThis.__statePerformanceTelemetry)?void 0:n.reuseCount)||0}:null},endFrame:(i,n)=>{if(!e||!i)return;t.refreshCount++,t.totalNodes+=n;const l=performance.now()-i.start;t.refreshTime+=l;const s=globalThis.__statePerformanceTelemetry;if(s){const e=s.scanCount-i.scan,t=s.moveCount-i.move,o=s.reuseCount-i.reuse;(e>0||l>2)&&console.log(`[DataTable Frame] Time: ${l.toFixed(2)}ms, Scans: ${e}, Moves: ${t}, Reuses: ${o}, Rows: ${n}`)}}}})();l.perf=s.stats;const o=((e,t)=>{let i=null,n=null,l=[];const s=(e,t)=>!!(i&&e>=i.minRow&&e<=i.maxRow&&t>=i.minCol&&t<=i.maxCol)||l.some(i=>e>=i.minRow&&e<=i.maxRow&&t>=i.minCol&&t<=i.maxCol);let o=!1;const a=()=>{if(globalThis.__DT_FEATURES__&&!globalThis.__DT_FEATURES__.selection)return;let n=1/0,a=-1/0;i&&(n=Math.min(n,i.minRow),a=Math.max(a,i.maxRow)),l.forEach(e=>{n=Math.min(n,e.minRow),a=Math.max(a,e.maxRow)});const r=n!==1/0;if(!r&&!o)return;o=r;const d=e.querySelector(".dt-body");d&&d.querySelectorAll(".dt-body-row").forEach(e=>{var i;const l=((null==(i=e._ref)?void 0:i.rIdx)??-1)+t._listStartIndex,o=e.querySelectorAll(".dt-cell");!r||la?o.forEach(e=>e.classList.remove("dt-cell-selected")):o.forEach((e,t)=>{s(l,t)?e.classList.add("dt-cell-selected"):e.classList.remove("dt-cell-selected")})})},r=()=>{let e=0;i&&(e+=i.maxRow-i.minRow+1),l.forEach(t=>e+=t.maxRow-t.minRow+1),t.selectedRowCount=e},d=(e=!1)=>{e||(i=null,n=null,l=[],a(),r())},c=()=>{if(!i)return null;let e=i.minRow,t=i.maxRow,n=i.minCol,s=i.maxCol;return l.forEach(i=>{e=Math.min(e,i.minRow),t=Math.max(t,i.maxRow),n=Math.min(n,i.minCol),s=Math.max(s,i.maxCol)}),{minRow:e,maxRow:t,minCol:n,maxCol:s}};return{applySelectionUI:a,clearAllActive:d,startSelect:(o,c,u)=>{const m=s(o,c),p=i&&(i.minRow!==i.maxRow||i.minCol!==i.maxCol)||l.length>0;u.shiftKey&&n?i={minRow:Math.min(n.row,o),maxRow:Math.max(n.row,o),minCol:Math.min(n.col,c),maxCol:Math.max(n.col,c)}:(!m||u.ctrlKey||u.metaKey?(u.ctrlKey||u.metaKey?i&&!m&&l.push(i):d(),n={row:o,col:c},i={minRow:o,maxRow:o,minCol:c,maxCol:c}):p||(e._potentialCancel={row:o,col:c}),t.isSelecting=!0),a(),r(),e.focus()},updateSelect:(l,s)=>{t.isSelecting&&n&&(i={minRow:Math.min(n.row,l),maxRow:Math.max(n.row,l),minCol:Math.min(n.col,s),maxCol:Math.max(n.col,s)},e._potentialCancel=null,a(),r())},endSelect:()=>{if(e._potentialCancel){const{row:t,col:i}=e._potentialCancel;s(t,i)&&d(),e._potentialCancel=null}t.isSelecting=!1},getSelectionBounds:c,copy:async()=>{const e=c();if(!e)return;const i=t.list.slice(e.minRow,e.maxRow+1).map(i=>t.fields.slice(e.minCol,e.maxCol+1).map(e=>{let t=String(i[e.id]??"");return(t.includes("\t")||t.includes("\n")||t.includes('"'))&&(t='"'+t.replace(/"/g,'""')+'"'),t}).join("\t")).join("\n");await navigator.clipboard.writeText(i)},paste:async()=>{try{const i=await navigator.clipboard.readText();if(!i)return;const n=c();if(!n)return;const l=i.split(/\r?\n/).filter(e=>e.length>0).map(e=>{const t=[];let i="",n=!1;for(let l=0;l{var t;return null==(t=e.classList)?void 0:t.contains("dt-body-row")});let u=!1;l.forEach((e,i)=>{const n=s+i;if(n>a||n>=t.list.length)return;const l=t.list[n];let d=!1;e.forEach((e,i)=>{const n=o+i;if(n>r||n>=t.fields.length)return;const s=t.fields[n];l[s.id]=e,d=!0}),d&&(u=!0)}),u&&(t.list=[...t.list])}catch(e){console.error("Paste Error:",e)}}}})(n,l),a=((e,t,i)=>{const n=globalThis.VirtualScroll({itemHeight:40});let l=null;const s=(e=!1)=>{if(!l)return;const s=n.calc(l,t.list);if(s){if(!e&&t.prevHeight===s.prevHeight&&t.postHeight===s.postHeight&&t._listStartIndex===s.listStartIndex&&t._renderedList.length===s.renderedList.length)return;Object.assign(t,{prevHeight:s.prevHeight,postHeight:s.postHeight,_listStartIndex:s.listStartIndex,_renderedList:s.renderedList}),null==i||i(s.renderedList.length,e)}};return{init:()=>{l=e.querySelector(".dt-main")},reset:i=>{t._listStartIndex=0,n.reset(i,l||e),t.list===i&&n.init(i,()=>s(!0))},refresh:s,onScroll:()=>s(!1)}})(n,l,()=>o.applySelectionUI()),r=n.querySelector(".dt-column-menu");r&&(r._thisObj=n),n.onColumnResizing=(e,t)=>n.style.setProperty(`--w-${e.id}`,t.detail.newSize+"px"),n.onColumnResize=(e,t)=>{const i=l.fields.findIndex(t=>t.id===e.id);-1!==i&&(l.fields[i].width=t.detail.newSize,l.fields=[...l.fields])};let d,c=null;n.format=(e,t)=>{var n;if(t.formatter)return t.formatter(e,t);const l=i._fieldTypes.get((null==(n=t.settings)?void 0:n.formType)||t.type||"text");return l&&l.formatter?l.formatter(e,t):null==e?"":"object"==typeof e?JSON.stringify(e):String(e)},n.onScroll=()=>{s.onScroll(),a.refresh(),n.hideColumnMenu();const e=n.querySelector(".dt-spacer-prev"),t=n.querySelector(".dt-spacer-post");e&&(e.style.height=(l.prevHeight||0)+"px",e.style.display=l.prevHeight>0?"block":"none"),t&&(t.style.height=(l.postHeight||0)+"px",t.style.display=l.postHeight>0?"block":"none")},n.applySortFilter=(e={})=>{if(l._internalUpdate)return;const t={...l.filterConfig,...e.filters||{}},i=void 0!==e.sort?e.sort?{fieldId:l.activeFieldId,direction:e.sort}:{fieldId:null,direction:null}:l.sortConfig;let n=[...l._originalList];if(Object.entries(t).forEach(([e,t])=>{(t.value||t.selectedValues&&0!==t.selectedValues.length)&&(n=n.filter(i=>{var n;const l=i[e];if((null==(n=t.selectedValues)?void 0:n.length)>0)return t.selectedValues.includes(String(l));const s=String(t.value).toLowerCase(),o=String(l??"").toLowerCase();switch(t.mode){case"contains":return o.includes(s);case"equals":return o===s;case"starts":return o.startsWith(s);case"ends":return o.endsWith(s);case"=":return Number(l)===Number(t.value);case">":return Number(l)>Number(t.value);case"<":return Number(l)=Number(t.value)&&Number(l)<=Number(t.value2);default:return!0}}))}),i.fieldId&&i.direction){const e=i.fieldId,t="asc"===i.direction?1:-1;n.sort((i,n)=>i[e]==n[e]?0:i[e]>n[e]?t:-t)}l._internalUpdate=!0,l.filterConfig=t,l.sortConfig=i,l.list=n,l._internalUpdate=!1},n.showColumnMenu=(t,i)=>{var s;const o=i.currentTarget,a=n.querySelector(".dt-column-menu"),r=(null==(s=t.settings)?void 0:s.formType)||t.type||"text";l.activeModes=e[r]||(["boolean","switch","checkbox","radio"].includes(r)?[]:e.text),l.filterConfig[t.id]||(l.filterConfig[t.id]={mode:l.activeModes[0]||"contains",value:"",selectedValues:[]}),l.activeField=t,l.activeFieldId=t.id,a.style.display="block";const d=o.closest(".dt-cell").getBoundingClientRect(),c=n.getBoundingClientRect(),u=a.offsetWidth||260;let m=d.right-c.left-u;m<0&&(m=Math.max(0,d.left-c.left)),a.style.left=m+"px",a.style.top=d.bottom-c.top+5+"px";const p=e=>{a.contains(e.target)||o.contains(e.target)||(n.hideColumnMenu(),n.applySortFilter(),document.removeEventListener("mousedown",p))};document.addEventListener("mousedown",p),setTimeout(()=>{var e;return null==(e=a.querySelector("input"))?void 0:e.focus()},50)},n.toggleSelectedValue=e=>{const t=l.filterConfig[l.activeFieldId];if(!t)return;const i=t.selectedValues.indexOf(e);-1===i?t.selectedValues.push(e):t.selectedValues.splice(i,1),l.filterConfig={...l.filterConfig},n.applySortFilter()},n.filterOnlyThis=e=>{l.filterConfig[l.activeFieldId]={mode:"contains",value:"",selectedValues:[String(e)]},l.filterConfig={...l.filterConfig},n.applySortFilter()},n.hideColumnMenu=()=>{const e=n.querySelector(".dt-column-menu");e&&(e.style.display="none")},n.setSort=e=>{const t=l.sortConfig.direction===e&&l.sortConfig.fieldId===l.activeFieldId?null:e;n.applySortFilter({sort:t})},n.clearColumnSettings=()=>{l.activeFieldId&&(delete l.filterConfig[l.activeFieldId],l.filterConfig={...l.filterConfig},n.applySortFilter())},n._initRow=e=>{var t;const i=null==(t=e._ref)?void 0:t.item;i&&void 0===i._editingF&&Object.defineProperty(i,"_editingF",{set:e=>{null===e&&n.hideEditor(!0)},configurable:!0}),Array.from(e.children).forEach(t=>{const i=parseInt(t.dataset.fidx);isNaN(i)||(t._ref={...t._ref||e._ref,f:l.fields[i],fIdx:i})})},l.__watch("fields",e=>{if(!e)return;l._fieldsDirty=!0,l._masterCellNodes=null,n.style.setProperty("--dt-grid-template",e.map(e=>{var t;return`var(--w-${e.id}, ${(null==(t=e.settings)?void 0:t.width)||e.width||150}px)`}).join(" ")),n.style.setProperty("--dt-row-width",e.reduce((e,t)=>{var i;return e+((null==(i=t.settings)?void 0:i.width)||t.width||150)},0)+"px");let t=0;e.forEach(e=>{var i,l;"left"===((null==(i=e.settings)?void 0:i.pinned)||e.pinned)&&(n.style.setProperty(`--l-${e.id}`,t+"px"),t+=(null==(l=e.settings)?void 0:l.width)||e.width||150)});let i=0;[...e].reverse().forEach(e=>{var t,l;"right"===((null==(t=e.settings)?void 0:t.pinned)||e.pinned)&&(n.style.setProperty(`--r-${e.id}`,i+"px"),i+=(null==(l=e.settings)?void 0:l.width)||e.width||150)})}),l.__watch("list",e=>{var t;if(l._fieldsDirty){l._fieldsDirty=!1;const e=null==(t=n.querySelector('.dt-body template[index="rIdx"]'))?void 0:t.content.querySelector('template[as="f"]');if(e){const t=l._masterCellNodes||(l._masterCellNodes=Array.from(e.content.childNodes).map(e=>e.cloneNode(!0)));e.removeAttribute("$each"),e.setAttribute("$if","true"),e.content.textContent="",l.fields.forEach((i,n)=>t.forEach(t=>{var l;const s=t.cloneNode(!0);if(1===s.nodeType){s.dataset.fidx=n;const e=(null==(l=i.settings)?void 0:l.pinned)||i.pinned;e&&(s.classList.add("pinned-"+e),s.style.position="sticky",s.style.zIndex="1",s.style.backgroundColor="inherit","left"===e?(s.style.left=`var(--l-${i.id})`,s.style.borderRight="1px solid var(--bs-border-color)",s.style.boxShadow="2px 0 5px -2px rgba(0,0,0,0.1)"):(s.style.right=`var(--r-${i.id})`,s.style.borderLeft="1px solid var(--bs-border-color)",s.style.boxShadow="-2px 0 5px -2px rgba(0,0,0,0.1)"))}e.content.appendChild(s)}))}}l._internalUpdate||(l._originalList=[...e||[]],setTimeout(()=>{const e={};l.fields.forEach(t=>{const i={};l._originalList.forEach(e=>{const n=e[t.id],l=null==n||""===n?"":String(n);i[l]=(i[l]||0)+1}),e[t.id]=Object.entries(i).sort((e,t)=>t[1]-e[1]).slice(0,20).map(([e,t])=>({val:e,count:t}))}),l._columnStats=e},200)),a.init(),a.reset(e)}),n.editCell=(e,t,i)=>{var l,s;const o=n.querySelector(".dt-editor-overlay"),a=i.getBoundingClientRect(),r=n.getBoundingClientRect();c=i;const d=(null==(l=t.settings)?void 0:l.formType)||t.type||"text",u=o.querySelector("AutoForm");u&&(u.data=e,u.state.schema=[{...t,type:d,options:(null==(s=t.settings)?void 0:s.options)||t.options,name:t.id,label:""}]),Object.assign(o.style,{display:"flex",left:a.left-r.left+"px",top:a.top-r.top+"px",width:("textarea"===d||"TagsInput"===d?Math.max(a.width,300):a.width)+"px",height:"textarea"===d||"TagsInput"===d?"auto":a.height+"px"}),setTimeout(()=>{var e;return null==(e=o.querySelector("input, textarea, select, .form-control"))?void 0:e.focus()},30)},n.hideEditor=(e=!0)=>{if(d||(d=n.querySelector(".dt-editor-overlay")),!d||"none"===d.style.display)return;const t=d.querySelector("AutoForm");if(e&&t&&t.data){const e=d.querySelector("input:focus, select:focus, textarea:focus");e&&e.dispatchEvent(new Event("number"===e.type||"SELECT"===e.tagName?"change":"input",{bubbles:!0}));if((t.state.schema||[]).forEach(e=>{var i,n;const l=null==(n=null==(i=null==c?void 0:c.closest(".dt-row"))?void 0:i._ref)?void 0:n.item;l&&(l[e.name]=t.data[e.name])}),l.isBulkEdit){const{minRow:e,maxRow:i,fIdx:n}=l.isBulkEdit,s=l.fields[n],o=t.data[s.id];for(let t=e;t<=i;t++)l.list[t]&&(l.list[t][s.id]=o)}l.list=[...l.list],l.isDirty=!0}d.style.display="none",t&&(t.state.schema=[],t.data=null),c=null,l.isBulkEdit=null,n.focus()},n.onMainMouseDown=e=>{var t;const i=e.target.closest(".dt-cell"),s=null==i?void 0:i.closest(".dt-row");if(!s||s.classList.contains("dt-header-row"))return;const a=i.dataset.fidx?parseInt(i.dataset.fidx):Array.from(s.querySelectorAll(".dt-cell")).indexOf(i),r=(null==(t=s._ref)?void 0:t.rIdx)??Array.from(n.querySelectorAll(".dt-body-row")).indexOf(s);o.startSelect(r+l._listStartIndex,a,e)},n.onMainMouseOver=e=>{var t;if(l.isSelecting){const i=e.target.closest(".dt-cell"),s=null==i?void 0:i.closest(".dt-row");if(s&&!s.classList.contains("dt-header-row")){const e=i.dataset.fidx?parseInt(i.dataset.fidx):Array.from(s.querySelectorAll(".dt-cell")).indexOf(i),a=(null==(t=s._ref)?void 0:t.rIdx)??Array.from(n.querySelectorAll(".dt-body-row")).indexOf(s);o.updateSelect(a+l._listStartIndex,e)}}},n.onMainDblClick=e=>{var t,i,s;const a=e.target.closest(".dt-cell"),r=null==a?void 0:a.closest(".dt-row");if(r&&!r.classList.contains("dt-header-row")){const e=null==(t=r._ref)?void 0:t.item,d=a.dataset.fidx?parseInt(a.dataset.fidx):Array.from(r.querySelectorAll(".dt-cell")).indexOf(a),c=((null==(i=r._ref)?void 0:i.rIdx)??Array.from(n.querySelectorAll(".dt-body-row")).indexOf(r))+l._listStartIndex;if(e&&l.fields[d]){const t=o.getSelectionBounds();if(t&&c>=t.minRow&&c<=t.maxRow&&d>=t.minCol&&d<=t.maxCol){const e=t.maxRow-t.minRow+1;e>1&&(l.isBulkEdit={...t,fIdx:d},(null==(s=globalThis.UI)?void 0:s.toast)&&globalThis.UI.toast(`Bulk Edit: Updating ${e} rows in column "${l.fields[d].name}"`,{type:"warning"}))}n.editCell(e,l.fields[d],a)}}},n.addRow=()=>{const e={};l.fields.forEach(t=>e[t.id]=""),l._originalList.push(e),l.list=[...l._originalList],l.isDirty=!0,setTimeout(()=>{a.reset(l.list),n.querySelector(".dt-main").scrollTop=n.querySelector(".dt-main").scrollHeight},50)},n.deleteSelectedRow=async()=>{const e=o.getSelectionBounds();if(!e)return;const t=e.maxRow-e.minRow+1;if(await globalThis.UI.confirm(`Are you sure you want to delete ${t} row(s)?`)){const t=e.minRow,i=e.maxRow,s=l.list.slice(t,i+1);l.list=l.list.filter((e,n)=>!(n>=t&&n<=i)),l._originalList=l._originalList.filter(e=>!s.includes(e)),l.isDirty=!0,o.clearAllActive(),n.dispatchEvent(new CustomEvent("remove",{detail:{items:s}}))}},n.saveChanges=()=>{n.dispatchEvent(new CustomEvent("save",{detail:{list:l._originalList,fields:l.fields}})),l.isDirty=!1};const u=()=>{const e=globalThis.DataTable.getFieldTypes(),t=[{name:"id",label:"Field ID",type:"text",setting:{required:!0,placeholder:"e.g. user_name"}},{name:"name",label:"Display Name",type:"text",setting:{required:!0,placeholder:"e.g. 用户名"}},{name:"user_type",label:"Field Type",type:"select",options:e.map(e=>({label:e.label,value:e.value}))}],i=e.reduce((e,t)=>e.concat(t.schema||[]),[]);return t.concat(i,[{name:"isIndex",label:"Index",type:"switch"},{name:"memo",label:"Memo",type:"text"}])},m=e=>{if(e)return e.split("\n").map(e=>e.trim()).filter(Boolean).map(e=>{const t=e.indexOf(":");return t>-1?{label:e.slice(0,t).trim(),value:e.slice(t+1).trim()}:e})};n.addField=async()=>{n.hideColumnMenu();const e=globalThis.NewState({id:"c"+Date.now().toString().slice(-4),name:"New Field",user_type:"text",decimals:0,isIndex:!1,memo:"",options_str:""}),t=n.querySelector(`Dialog[id="${n.id}_field_dialog"]`);if(!t)return;Object.assign(t.state,{title:"Add Field",buttons:["Cancel","Save"]});const i=t.querySelector("AutoForm");i&&(i.data=e,i.state.schema=u()),t.show();if(2===await new Promise(e=>t.addEventListener("change",i=>e(t.result),{once:!0}))){const t=globalThis.DataTable.getFieldTypes().find(t=>t.value===e.user_type);let i=(null==t?void 0:t.typeForDB)||"v1024";"number"===e.user_type&&(i=e.decimals>0?"ff":"bi");const s={id:e.id,name:e.name,memo:e.memo,isIndex:!!e.isIndex,type:i,settings:{formType:e.user_type,decimals:e.decimals,prefix:e.prefix,suffix:e.suffix,thousandSep:e.thousandSep,labelOn:e.labelOn,labelOff:e.labelOff,format:e.format,placeholder:e.placeholder,options:m(e.options_str)}};l.fields=[...l.fields,s],l.isDirty=!0,n.dispatchEvent(new CustomEvent("savefields",{detail:l.fields})),l.list=[...l.list]}},n.editField=async()=>{if(!l.activeField)return;n.hideColumnMenu();const e=l.activeField,t=e.settings||{},i=globalThis.NewState({id:e.id,name:e.name,memo:e.memo||"",isIndex:!!e.isIndex,user_type:t.formType||"text",decimals:t.decimals||0,prefix:t.prefix||"",suffix:t.suffix||"",thousandSep:!!t.thousandSep,labelOn:t.labelOn||"",labelOff:t.labelOff||"",format:t.format||"",placeholder:t.placeholder||"",options_str:(s=t.options,s?s.map(e=>"object"==typeof e?`${e.label}:${e.value}`:e).join("\n"):"")});var s;const o=n.querySelector(`Dialog[id="${n.id}_field_dialog"]`);if(!o)return;Object.assign(o.state,{title:"Edit Field",buttons:["Cancel","Save"]});const a=o.querySelector("AutoForm");a&&(a.data=i,a.state.schema=u()),o.show();if(2===await new Promise(e=>o.addEventListener("change",t=>e(o.result),{once:!0}))){const t=l.fields.findIndex(t=>t.id===e.id);if(-1!==t){const s=globalThis.DataTable.getFieldTypes().find(e=>e.value===i.user_type);let o=(null==s?void 0:s.typeForDB)||"v1024";"number"===i.user_type&&(o=i.decimals>0?"ff":"bi");const a={...e,id:i.id,name:i.name,memo:i.memo,isIndex:!!i.isIndex,type:o,settings:{...e.settings,formType:i.user_type,decimals:i.decimals,prefix:i.prefix,suffix:i.suffix,thousandSep:i.thousandSep,labelOn:i.labelOn,labelOff:i.labelOff,format:i.format,placeholder:i.placeholder,options:m(i.options_str)}};l.fields[t]=a,l.fields=[...l.fields],l.isDirty=!0,n.dispatchEvent(new CustomEvent("savefields",{detail:l.fields})),l.list=[...l.list]}}},n.deleteField=async()=>{if(l.activeField&&(n.hideColumnMenu(),await globalThis.UI.confirm(`Are you sure you want to delete field "${l.activeField.name}"?`))){const e=l.fields.findIndex(e=>e.id===l.activeField.id);-1!==e&&(l.fields.splice(e,1),l.fields=[...l.fields],l.isDirty=!0,n.dispatchEvent(new CustomEvent("savefields",{detail:l.fields})),l.list=[...l.list])}},window.addEventListener("mouseup",o.endSelect),document.addEventListener("mousedown",e=>{const t=n.querySelector(".dt-editor-overlay"),i=n.querySelector(".dt-column-menu");"none"===(null==t?void 0:t.style.display)||t.contains(e.target)||n.hideEditor(!0),n.contains(e.target)||(null==t?void 0:t.contains(e.target))||(null==i?void 0:i.contains(e.target))||o.clearAllActive()}),l._MODE_ICONS=t},globalThis.Util.makeDom('\n\n
\n\n
\n
\n \n \n \n
\n\n \n\n \n \n \n \n
\n \n\n \n
TOP FREQUENT VALUES
\n
\n \n \n
\n\n \n Clear Filter\n
\n\n \n \n \n \n
\n \n
\n\n
\n\n
\n\n
\n
\n'),globalThis.Util.makeDom("\n\n")),globalThis.DataTable=i});
diff --git a/test/lib/state.js b/test/lib/state.js
new file mode 100644
index 0000000..e161a0d
--- /dev/null
+++ b/test/lib/state.js
@@ -0,0 +1,661 @@
+(function(global, factory) {
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoState = global.ApigoState || {}));
+})(this, function(exports2) {
+ "use strict";
+ var _a, _b;
+ const Util = {
+ clone: globalThis.structuredClone || ((obj) => JSON.parse(JSON.stringify(obj))),
+ base64: (str) => btoa(String.fromCharCode(...new TextEncoder().encode(str))),
+ unbase64: (str) => new TextDecoder().decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0))),
+ urlbase64: (str) => Util.base64(str).replace(/[+/=]/g, (m) => ({ "+": "-", "/": "", "=": "" })[m]),
+ unurlbase64: (str) => Util.unbase64(str.replace(/[-_.]/g, (m) => ({ "-": "+", "_": "/", ".": "=" })[m]).padEnd(Math.ceil(str.length / 4) * 4, "=")),
+ safeJson: (str) => {
+ try {
+ return JSON.parse(str);
+ } catch {
+ return null;
+ }
+ },
+ updateDefaults: (obj, defaults) => {
+ for (const k in defaults) if (obj[k] === void 0) obj[k] = defaults[k];
+ },
+ copyFunction: (toObj, fromObj, ...funcNames) => {
+ funcNames.forEach((name) => toObj[name] = fromObj[name].bind(fromObj));
+ },
+ getFunctionBody: (fn) => {
+ const code = fn.toString();
+ return code.slice(code.indexOf("{") + 1, code.lastIndexOf("}")).trim();
+ },
+ makeDom: (html) => {
+ if (html.includes(">\n")) html = html.replace(/>\s+<").trim();
+ const node = document.createElement("div");
+ node.innerHTML = html;
+ return node.children[0];
+ },
+ newAvg: () => {
+ let total = 0, count = 0, avg = 0;
+ return {
+ add: (v) => {
+ total += v;
+ count++;
+ return avg = total / count;
+ },
+ get: () => avg,
+ clear: () => {
+ total = 0, count = 0, avg = 0;
+ }
+ };
+ },
+ newTimeCount: () => {
+ let startTime = 0, total = 0, count = 0;
+ return {
+ start: () => startTime = (/* @__PURE__ */ new Date()).getTime(),
+ end: () => {
+ const endTime = (/* @__PURE__ */ new Date()).getTime();
+ const left = endTime - startTime;
+ startTime = endTime;
+ total += left;
+ count++;
+ return left;
+ },
+ avg: () => total / count
+ };
+ }
+ };
+ const $ = (a, b) => b ? a.querySelector(b) : document.querySelector(a);
+ const $$ = (a, b) => b ? a.querySelectorAll(b) : document.querySelectorAll(a);
+ globalThis.Util = Util;
+ globalThis.$ = $;
+ globalThis.$$ = $$;
+ let __activeBinding = null;
+ let __noWriteBack = null;
+ const _setActiveBinding = (val) => __activeBinding = val;
+ const _setNoWriteBack = (val) => __noWriteBack = val;
+ const _notifiers = /* @__PURE__ */ new Set();
+ const _onNotifyUpdate = (fn) => _notifiers.add(fn);
+ function NewState(defaults = {}, getter = null, setter = null) {
+ const _defaults = {};
+ const _stateMappings = /* @__PURE__ */ new Map();
+ const _watchers = /* @__PURE__ */ new Map();
+ const _watchFunc = (k, cb) => {
+ if (!_watchers.has(k)) _watchers.set(k, /* @__PURE__ */ new Set());
+ !cb ? _watchers.get(k).clear() : _watchers.get(k).add(cb);
+ return () => _watchers.get(k).delete(cb);
+ };
+ const _unwatchFunc = (k, cb) => {
+ if (_watchers.has(k)) _watchers.set(k, /* @__PURE__ */ new Set());
+ _watchers.get(k).delete(cb);
+ };
+ const __getter = getter || ((k) => _defaults[k]);
+ const __setter = setter || ((k, v) => _defaults[k] = v);
+ Object.assign(_defaults, defaults);
+ return new Proxy(_defaults, {
+ get(target, key) {
+ if (key === "__watch") return _watchFunc;
+ if (key === "__unwatch") return _unwatchFunc;
+ if (key === "__isProxy") return true;
+ if (__activeBinding) {
+ if (!_stateMappings.has(key)) _stateMappings.set(key, /* @__PURE__ */ new Set());
+ _stateMappings.get(key).add(__activeBinding);
+ if (!__activeBinding.node._states) __activeBinding.node._states = /* @__PURE__ */ new Set();
+ __activeBinding.node._states.add(_stateMappings);
+ }
+ return __getter(key);
+ },
+ set(target, key, value) {
+ if (__getter(key) !== value) {
+ __setter(key, value);
+ }
+ if (_watchers.has(key)) {
+ _watchers.get(key).forEach((cb) => {
+ const r = cb(value);
+ if (r !== void 0) {
+ value = r;
+ target[key] = value;
+ }
+ });
+ }
+ if (_watchers.has(null)) {
+ _watchers.get(null).forEach((cb) => cb(value));
+ }
+ if (_stateMappings.has(key)) {
+ const bindings = _stateMappings.get(key);
+ for (const binding of bindings) {
+ if (!binding.node.isConnected) {
+ bindings.delete(binding);
+ continue;
+ }
+ if (__noWriteBack !== binding.node) {
+ _notifiers.forEach((fn) => fn(binding));
+ }
+ }
+ }
+ return true;
+ }
+ });
+ }
+ globalThis.NewState = NewState;
+ let _hashParams = new URLSearchParams(typeof globalThis !== "undefined" ? ((_b = (_a = globalThis.location) == null ? void 0 : _a.hash) == null ? void 0 : _b.substring(1)) || "" : "");
+ const Hash = NewState({}, (k) => Util.safeJson(_hashParams.get(k)), (k, v) => {
+ const oldStr = _hashParams.get(k);
+ const newStr = v === void 0 ? void 0 : JSON.stringify(v);
+ if (oldStr === newStr || oldStr === null && newStr === void 0) return;
+ v === void 0 ? _hashParams.delete(k) : _hashParams.set(k, newStr);
+ globalThis.location.hash = "#" + _hashParams.toString();
+ });
+ if (typeof globalThis !== "undefined") {
+ globalThis.addEventListener("hashchange", () => {
+ var _a2;
+ const newParams = new URLSearchParams(((_a2 = globalThis.location.hash) == null ? void 0 : _a2.substring(1)) || "");
+ const keys = /* @__PURE__ */ new Set([..._hashParams.keys(), ...newParams.keys()]);
+ _hashParams = newParams;
+ keys.forEach((k) => Hash[k] = Hash[k]);
+ });
+ }
+ const LocalStorage = NewState({}, (k) => Util.safeJson(localStorage.getItem(k)), (k, v) => {
+ const oldStr = localStorage.getItem(k);
+ const newStr = v === void 0 ? void 0 : JSON.stringify(v);
+ if (oldStr === newStr || oldStr === null && newStr === void 0) return;
+ v === void 0 ? localStorage.removeItem(k) : localStorage.setItem(k, newStr);
+ });
+ const State = NewState({
+ exitBlocks: 0
+ });
+ globalThis.Hash = Hash;
+ globalThis.LocalStorage = LocalStorage;
+ globalThis.State = State;
+ let _disableRunCodeError = false;
+ const setDisableRunCodeError = (value) => {
+ _disableRunCodeError = value;
+ };
+ const _fnCache = /* @__PURE__ */ new Map();
+ function _runCode(code, vars, thisObj, extendVars) {
+ const allVars = { ...extendVars || {}, ...vars || {} };
+ const argKeys = Object.keys(allVars);
+ const argValues = Object.values(allVars);
+ const cacheKey = code + argKeys.join(",");
+ try {
+ let fn = _fnCache.get(cacheKey);
+ if (!fn) {
+ fn = new Function("Hash", "LocalStorage", "State", ...argKeys, code);
+ _fnCache.set(cacheKey, fn);
+ }
+ return fn.apply(thisObj, [globalThis.Hash, globalThis.LocalStorage, globalThis.State, ...argValues]);
+ } catch (e) {
+ if (!_disableRunCodeError) console.error(e, extendVars, [code, extendVars, vars, thisObj]);
+ return null;
+ }
+ }
+ function _returnCode(code, vars, thisObj, extendVars) {
+ if (code.includes("${")) return _runCode("return `" + code + "`", vars, thisObj, extendVars);
+ else return _runCode("return " + code, vars, thisObj, extendVars);
+ }
+ const _components = /* @__PURE__ */ new Map();
+ const _pendingTemplates = [];
+ const Component = {
+ getTemplate: (name) => document.querySelector(`template[component="${name.toUpperCase()}"]`),
+ register: (name, setupFunc, templateNode = null, ...globalNodes) => {
+ _components.set(name.toUpperCase(), setupFunc);
+ if (document.readyState !== "loading") Component._addTemplate(name, templateNode, globalNodes);
+ else _pendingTemplates.push([name, templateNode, globalNodes]);
+ },
+ exists: (name) => _components.has(name.toUpperCase()),
+ getSetupFunction: (name) => _components.get(name.toUpperCase()),
+ _addTemplate: (name, templateNode, globalNodes) => {
+ if (templateNode) {
+ const template = document.createElement("TEMPLATE");
+ template.setAttribute("component", name.toUpperCase());
+ template.content.appendChild(templateNode);
+ document.body.appendChild(template);
+ }
+ if (globalNodes) globalNodes.forEach((node) => document.body.appendChild(node));
+ },
+ _initPending: () => {
+ _pendingTemplates.forEach(([name, templateNode, globalNodes]) => Component._addTemplate(name, templateNode, globalNodes));
+ _pendingTemplates.length = 0;
+ }
+ };
+ function _mergeNode(from, to, scanObj, exists = {}) {
+ if (from.attributes) {
+ Array.from(from.attributes).forEach((attr) => {
+ if (attr.name === "class") return;
+ if (attr.name === "style") {
+ if (to.hasAttribute("style")) to.setAttribute("style", `${attr.value}; ${to.getAttribute("style")}`);
+ else to.setAttribute("style", attr.value);
+ } else if (!to.hasAttribute(attr.name)) {
+ to.setAttribute(attr.name, attr.value);
+ }
+ });
+ }
+ to.classList.add(...from.classList);
+ const target = to.tagName === "TEMPLATE" ? to.content : to;
+ const sourceNodes = from.tagName === "TEMPLATE" ? from.content.childNodes : from.childNodes;
+ Array.from(sourceNodes).forEach((child) => target.appendChild(child));
+ if (from.tagName && Component.exists(from.tagName)) _makeComponent(from.tagName, to, scanObj, exists);
+ }
+ function _makeComponent(name, node, scanObj, exists = {}) {
+ if (exists[name]) return;
+ exists[name] = true;
+ if (scanObj.thisObj) {
+ Array.from(node.attributes).forEach((attr) => {
+ if ((attr.name.startsWith("$") || attr.name.startsWith("st-")) && attr.value.includes("this.")) {
+ attr.value = attr.value.replace(/\bthis\./g, "this.parent.");
+ }
+ });
+ }
+ const componentFunc = Component.getSetupFunction(name);
+ const slots = {};
+ Array.from(node.childNodes).forEach((child) => {
+ if (child.nodeType === Node.ELEMENT_NODE && child.hasAttribute("slot")) {
+ slots[child.getAttribute("slot")] = child;
+ child.removeAttribute("slot");
+ }
+ });
+ node.innerHTML = "";
+ node.state = NewState(node.state || {});
+ const template = Component.getTemplate(name);
+ if (template) {
+ const tplnode = template.content.cloneNode(true);
+ if (tplnode.childNodes.length) {
+ const rootNode = tplnode.children[0];
+ if (rootNode) _mergeNode(rootNode, node, scanObj, exists);
+ $$(node, "[slot-id]").forEach((placeholder) => {
+ const slotName = placeholder.getAttribute("slot-id");
+ if (slots[slotName]) {
+ placeholder.removeAttribute("slot-id");
+ placeholder.innerHTML = "";
+ _mergeNode(slots[slotName], placeholder, scanObj, exists);
+ }
+ });
+ }
+ }
+ if (componentFunc) componentFunc(node);
+ }
+ let _translator = (text, args) => {
+ if (!text || typeof text !== "string") return text;
+ return text.replace(/\{(.+?)\}/g, (match, key) => args.hasOwnProperty(key) ? args[key] : match);
+ };
+ const SetTranslator = (fn) => _translator = fn;
+ const _translate = (text) => {
+ if (!text || typeof text !== "string" || !text.includes("{#")) return text;
+ return text.replace(/\{#(.+?)#\}/g, (m, content) => {
+ const parts = content.split("||").map((s) => s.trim());
+ const args = {};
+ if (parts.length > 1) {
+ const matches = parts[0].match(/\{(.+?)\}/g);
+ if (matches) matches.forEach((match, i) => args[match.substring(1, match.length - 1)] = parts[i + 1] || "");
+ }
+ return _translator(parts[0], args);
+ });
+ };
+ if (typeof document !== "undefined") {
+ try {
+ document.createElement("div").setAttribute("$t", "1");
+ } catch (e) {
+ const originalSetAttribute = Element.prototype.setAttribute;
+ Element.prototype.setAttribute = function(name, value) {
+ if (!name.startsWith("$")) return originalSetAttribute.call(this, name, value);
+ return originalSetAttribute.call(this, "st-" + name.substring(1), value);
+ };
+ }
+ }
+ _onNotifyUpdate((binding) => _updateBinding(binding));
+ function _clearRenderedNodes(node) {
+ if (node._renderedNodes) node._renderedNodes.forEach((nodes) => nodes.forEach((child) => {
+ child.remove();
+ if (child._renderedNodes) _clearRenderedNodes(child);
+ }));
+ }
+ function _updateBinding(binding) {
+ const node = binding.node;
+ if (!node.isConnected && node.tagName !== "TEMPLATE") return;
+ _setActiveBinding(binding);
+ let result = binding.exp ? binding.tpl ? _returnCode(binding.tpl, { thisNode: node }, node._thisObj || node, node._ref || null) : null : binding.tpl;
+ if (binding.exp === 2 && typeof result === "string") {
+ try {
+ result = _returnCode(result, { thisNode: node }, node._thisObj || node, node._ref || null);
+ } catch (e) {
+ }
+ }
+ _setActiveBinding(null);
+ if (binding.prop) {
+ const prop = binding.prop;
+ let o = node;
+ for (let i = 0; i < prop.length - 1; i++) {
+ if (!prop[i]) continue;
+ if (o[prop[i]] == null) o[prop[i]] = {};
+ o = o[prop[i]];
+ if (typeof o !== "object") break;
+ }
+ if (typeof o === "object" && o !== null) {
+ const lk = prop[prop.length - 1];
+ if (lk) {
+ if (typeof result === "object" && result != null && !Array.isArray(result) && o[lk] == null) o[lk] = {};
+ const lo = o[lk];
+ if (typeof lo === "object" && lo != null && lo.__watch) Object.assign(lo, result);
+ else {
+ if (o[lk] !== result) o[lk] = result;
+ }
+ } else if (typeof result === "object" && result != null && !Array.isArray(result)) {
+ Object.assign(o, result);
+ }
+ }
+ } else if (binding.attr) {
+ const attr = binding.attr;
+ if (attr === "if") {
+ if (result) {
+ if (!node._renderedNodes || node._renderedNodes.length === 0) {
+ node._children.forEach((child) => {
+ node.parentNode.insertBefore(child, node);
+ child._ref = { ...node._ref };
+ child._thisObj = node._thisObj;
+ });
+ node._renderedNodes = [node._children];
+ }
+ } else {
+ _clearRenderedNodes(node);
+ node._renderedNodes = [];
+ }
+ } else if (attr === "each") {
+ if (result && typeof result === "object") {
+ const asName = node.getAttribute("as") || "item";
+ const indexName = node.getAttribute("index") || "index";
+ const keyName = node.getAttribute("key");
+ let keys, getVal;
+ if (result instanceof Map) {
+ keys = Array.from(result.keys());
+ getVal = (k) => result.get(k);
+ } else if (typeof result[Symbol.iterator] === "function") {
+ const arr = Array.isArray(result) ? result : Array.from(result);
+ keys = new Array(arr.length);
+ for (let i = 0; i < arr.length; i++) keys[i] = i;
+ getVal = (k) => arr[k];
+ } else {
+ keys = Object.keys(result);
+ getVal = (k) => result[k];
+ }
+ if (!node._keyedNodes) node._keyedNodes = /* @__PURE__ */ new Map();
+ const newKeyedNodes = /* @__PURE__ */ new Map();
+ const currentRenderedNodes = [];
+ keys.forEach((k, i) => {
+ const item = getVal(k);
+ const rawKey = keyName ? item && typeof item === "object" ? item[keyName] : item : k;
+ const keyVal = rawKey === void 0 || rawKey === null || newKeyedNodes.has(rawKey) ? `st_key_${i}` : rawKey;
+ let existingNodes = node._keyedNodes.get(keyVal);
+ if (existingNodes) {
+ node._keyedNodes.delete(keyVal);
+ existingNodes.forEach((child) => {
+ child._ref[indexName] = k;
+ child._ref[asName] = item;
+ _scanTree(child);
+ });
+ } else {
+ existingNodes = [];
+ node._children.forEach((child) => {
+ const cloned = child.cloneNode(true);
+ cloned._ref = { ...node._ref, [indexName]: k, [asName]: item };
+ cloned._thisObj = node._thisObj;
+ node.parentNode.insertBefore(cloned, node);
+ existingNodes.push(cloned);
+ });
+ }
+ newKeyedNodes.set(keyVal, existingNodes);
+ currentRenderedNodes.push(existingNodes);
+ });
+ node._keyedNodes.forEach((nodes) => nodes.forEach((child) => {
+ _clearRenderedNodes(child);
+ child.remove();
+ }));
+ node._keyedNodes = newKeyedNodes;
+ node._renderedNodes = currentRenderedNodes;
+ } else {
+ _clearRenderedNodes(node);
+ node._renderedNodes = [];
+ }
+ } else if (attr === "bind") {
+ if (["INPUT", "SELECT", "TEXTAREA"].includes(node.tagName) && !node.hasAttribute("autocomplete")) node.setAttribute("autocomplete", "off");
+ if (node.type === "checkbox") {
+ if (node.value !== "on" && !result) {
+ _runCode(`${binding.tpl} = []`, { thisNode: node }, node._thisObj || node, node._ref || {});
+ result = [];
+ }
+ node._checkboxMultiMode = result instanceof Array;
+ const isChecked = result instanceof Array ? result.includes(node.value) : !!result;
+ if (node.checked !== isChecked) node.checked = isChecked;
+ } else if (node.type === "radio") {
+ if (node.checked !== (node.value === String(result ?? ""))) node.checked = node.value === String(result ?? "");
+ } else if ("value" in node && node.type !== "file") {
+ Promise.resolve().then(() => {
+ if (node.value !== String(result ?? "")) node.value = result;
+ });
+ } else if (node.isContentEditable) {
+ if (node.innerHTML !== String(result ?? "")) node.innerHTML = result;
+ }
+ node.dispatchEvent(new CustomEvent("bind", { bubbles: false, detail: result }));
+ } else {
+ if (["checked", "disabled", "readonly"].includes(attr)) result = !!result;
+ if (typeof result === "boolean") result ? node.setAttribute(attr, "") : node.removeAttribute(attr);
+ else if (result !== void 0) {
+ if (typeof result !== "string") result = JSON.stringify(result);
+ if (attr === "text") node.textContent = result ?? "";
+ else if (attr === "html") node.innerHTML = result ?? "";
+ else if (node.tagName === "IMG" && attr === "src" && result.includes(".svg")) node.setAttribute("_src", result ?? "");
+ else node.setAttribute(attr, result ?? "");
+ }
+ }
+ }
+ }
+ function _initBinding(binding) {
+ if (!binding.node._bindings) binding.node._bindings = [];
+ binding.node._bindings.push({ attr: binding.attr, prop: binding.prop, tpl: binding.tpl, exp: binding.exp });
+ _updateBinding(binding);
+ }
+ function _parseNode(node, scanObj) {
+ if (node._bindings) {
+ node._states = /* @__PURE__ */ new Set();
+ node._bindings.forEach((b) => _updateBinding({ node, ...b }));
+ if (node._hasOnUpdate) node.dispatchEvent(new Event("update", { bubbles: false }));
+ return;
+ }
+ if (Component.exists(node.tagName) && !node._componentInitialized) {
+ Array.from(node.attributes).forEach((attr) => {
+ var _a2;
+ if (attr.name.startsWith("$.")) {
+ const realAttrName = attr.name.slice(2);
+ let tpl = _translate(attr.value);
+ if (scanObj.thisObj && tpl.includes("this.")) tpl = tpl.replace(/\bthis\./g, "this.parent.");
+ const result = _returnCode(tpl, { thisNode: node }, { parent: scanObj.thisObj || node }, node._ref || {});
+ let o = node;
+ const prop = realAttrName.split(".");
+ for (let i = 0; i < prop.length - 1; i++) {
+ if (prop[i]) o = o[_a2 = prop[i]] ?? (o[_a2] = {});
+ }
+ o[prop[prop.length - 1]] = result;
+ node.removeAttribute(attr.name);
+ }
+ });
+ _makeComponent(node.tagName, node, scanObj);
+ $$(node, "[slot-id]").forEach((p) => p.removeAttribute("slot-id"));
+ node._componentInitialized = true;
+ if (!node._thisObj) node._thisObj = node;
+ }
+ if (node.tagName === "TEMPLATE") {
+ node._children = [...node.content.childNodes];
+ if (!node._renderedNodes) node._renderedNodes = [];
+ }
+ let attrs = [];
+ if (node.tagName === "TEMPLATE") {
+ ["$if", "$each", "st-if", "st-each"].forEach((n) => node.hasAttribute(n) && attrs.push(node.getAttributeNode(n)));
+ } else {
+ attrs = Array.from(node.attributes).filter((a) => (a.name.startsWith("$") || a.name.startsWith("st-")) && !["$if", "$each", "st-if", "st-each"].includes(a.name) || a.name.includes("."));
+ }
+ if (node._thisObj && scanObj.thisObj && node._thisObj !== scanObj.thisObj) node._thisObj.parent = scanObj.thisObj;
+ if (!node._thisObj) node._thisObj = scanObj.thisObj || null;
+ if (!node._ref) node._ref = scanObj.extendVars || {};
+ node._states = /* @__PURE__ */ new Set();
+ attrs.forEach((attr) => {
+ let exp = 0;
+ if (attr.name.startsWith("$$") || attr.name.startsWith("st-st-")) exp = 2;
+ else if (attr.name.startsWith("$") || attr.name.startsWith("st-")) exp = 1;
+ const realAttrName = exp === 2 ? attr.name.startsWith("$$") ? attr.name.slice(2) : attr.name.slice(6) : exp === 1 ? attr.name.startsWith("$") ? attr.name.slice(1) : attr.name.slice(3) : attr.name;
+ let tpl = attr.value;
+ node.removeAttribute(attr.name);
+ if (realAttrName.startsWith(".")) _initBinding({ node, prop: realAttrName.split("."), tpl, exp });
+ else if (realAttrName.startsWith("on")) {
+ const eventName = realAttrName.slice(2);
+ if (eventName === "update") node._hasOnUpdate = true;
+ if (eventName === "load" && !["BODY", "IMG", "IFRAME"].includes(node.tagName)) node._hasOnLoad = true;
+ if (eventName === "unload" && !["BODY", "IMG", "IFRAME"].includes(node.tagName)) node._hasOnUnload = true;
+ node.addEventListener(eventName, (e) => _runCode(tpl, { event: e, thisNode: node, ...e.detail || {} }, scanObj.thisObj || node, node._ref || {}));
+ } else {
+ if (realAttrName === "bind") {
+ const isTextInput = ["INPUT", "TEXTAREA"].includes(node.tagName) && ["textarea", "text", "password", "email", "number", "search", "url", "tel"].includes(node.type || "text") || node.isContentEditable;
+ node.addEventListener(isTextInput ? "input" : "change", (e) => {
+ let newVal = node.isContentEditable ? e.target.innerHTML : node.type === "checkbox" ? e.target.checked : e.target.files || e.target.value || e.detail;
+ _setNoWriteBack(node);
+ setDisableRunCodeError(true);
+ if (node.type === "checkbox" && node._checkboxMultiMode) _runCode(`!!checked ? (!${tpl}.includes(val) && ${tpl}.push(val)) : (index = ${tpl}.indexOf(val), index > -1 && ${tpl}.splice(index, 1))`, { val: node.value, checked: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {});
+ else _runCode(`${tpl} = val`, { val: newVal, thisNode: node }, scanObj.thisObj || node, node._ref || {});
+ setDisableRunCodeError(false);
+ _setNoWriteBack(null);
+ });
+ } else if (realAttrName === "text" && !tpl) {
+ tpl = node.textContent;
+ node.textContent = "";
+ }
+ if (tpl) {
+ tpl = _translate(tpl);
+ _initBinding({ node, attr: realAttrName, tpl, exp });
+ }
+ }
+ });
+ if (node._hasOnLoad || node._componentInitialized) Promise.resolve().then(() => node.dispatchEvent(new Event("load", { bubbles: false })));
+ if (node._hasOnUpdate) node.dispatchEvent(new Event("update", { bubbles: false }));
+ if (node._thisObj) scanObj.thisObj = node._thisObj;
+ }
+ const _scanTree = (node, scanObj = {}) => {
+ if (node.nodeType === 3) {
+ if (node._stTranslated) return;
+ const translated = _translate(node.textContent);
+ if (translated !== node.textContent) node.textContent = translated;
+ node._stTranslated = true;
+ return;
+ }
+ if (node.nodeType !== 1) return;
+ if (!node._stTranslated) {
+ Array.from(node.attributes).forEach((attr) => {
+ if (!attr.name.startsWith("$") && !attr.name.startsWith("st-") && !attr.name.startsWith(".")) {
+ const translated = _translate(attr.value);
+ if (translated !== attr.value) attr.value = translated;
+ }
+ });
+ node._stTranslated = true;
+ }
+ if (node.tagName !== "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("$each") || node.hasAttribute("st-if") || node.hasAttribute("st-each"))) {
+ const template = document.createElement("TEMPLATE");
+ const attrs = Array.from(node.attributes).filter((attr) => ["$if", "$each", "st-if", "st-each"].includes(attr.name) || (node.hasAttribute("$each") || node.hasAttribute("st-each")) && ["as", "index"].includes(attr.name));
+ attrs.forEach((attr) => {
+ template.setAttribute(attr.name, attr.value);
+ node.removeAttribute(attr.name);
+ });
+ node.parentNode.insertBefore(template, node);
+ template.content.appendChild(node);
+ template._ref = node._ref;
+ _scanTree(template, scanObj);
+ return;
+ }
+ if (node.tagName === "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("st-if")) && (node.hasAttribute("$each") || node.hasAttribute("st-each"))) {
+ const template = document.createElement("TEMPLATE");
+ const attrs = Array.from(node.attributes).filter((attr2) => ["$if", "$each", "st-if", "st-each"].includes(attr2.name));
+ const attr = attrs[attrs.length - 1];
+ template.setAttribute(attr.name, attr.value);
+ node.removeAttribute(attr.name);
+ if (attr.name === "$each" || attr.name === "st-each") {
+ Array.from(node.attributes).filter((attr2) => ["as", "index"].includes(attr2.name)).forEach((attr2) => {
+ template.setAttribute(attr2.name, attr2.value);
+ node.removeAttribute(attr2.name);
+ });
+ }
+ Array.from(node.content.childNodes).forEach((child) => template.content.appendChild(child));
+ node.content.appendChild(template);
+ template._ref = node._ref;
+ }
+ if (node.tagName === "IMG" && (node.hasAttribute("src") || node.hasAttribute("_src") || node.hasAttribute("$src"))) {
+ const imgNode = node;
+ Promise.resolve().then(() => {
+ const url = imgNode.getAttribute("_src") || imgNode.getAttribute("src");
+ if (url) fetch(url, { cache: "force-cache" }).then((r) => r.text()).then((svgText) => {
+ const realSvg = new DOMParser().parseFromString(svgText, "image/svg+xml").querySelector("svg");
+ if (realSvg) {
+ Array.from(imgNode.attributes).forEach((attr) => realSvg.setAttribute(attr.name, attr.value));
+ imgNode.replaceWith(realSvg);
+ }
+ });
+ });
+ }
+ if (node._thisObj !== void 0) scanObj.thisObj = node._thisObj || null;
+ else {
+ let curr = node;
+ while (curr && curr._thisObj === void 0) curr = curr.parentNode;
+ scanObj.thisObj = curr ? curr._thisObj : null;
+ }
+ if (node._ref === void 0) {
+ let curr = node;
+ while (curr && curr._ref === void 0) curr = curr.parentNode;
+ node._ref = curr ? { ...curr._ref } : {};
+ }
+ if (scanObj.extendVars) Object.assign(node._ref, scanObj.extendVars);
+ _parseNode(node, { ...scanObj });
+ const nodes = [...node.childNodes || []];
+ nodes.forEach((child) => _scanTree(child, { thisObj: scanObj.thisObj, extendVars: { ...node._ref } }));
+ };
+ const _unbindTree = (node) => {
+ if (node.nodeType !== 1) return;
+ if (node._hasOnUnload) node.dispatchEvent(new Event("unload", { bubbles: false }));
+ if (node._states) node._states.forEach((mappings) => {
+ for (const [key, bindingSet] of mappings) {
+ for (const binding of bindingSet) {
+ if (binding.node === node) bindingSet.delete(binding);
+ }
+ }
+ });
+ node.childNodes && node.childNodes.forEach((child) => _unbindTree(child));
+ };
+ globalThis.Component = Component;
+ globalThis.SetTranslator = SetTranslator;
+ globalThis.__unsafeRefreshState = _scanTree;
+ if (typeof document !== "undefined") {
+ const init = () => {
+ if (globalThis.Component && globalThis.Component._initPending) {
+ globalThis.Component._initPending();
+ }
+ const htmlNode = document.documentElement;
+ if (!htmlNode.hasAttribute("$data-bs-theme") && !htmlNode.hasAttribute("data-bs-theme")) {
+ htmlNode.setAttribute("$data-bs-theme", "LocalStorage.darkMode?'dark':'light'");
+ }
+ new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ mutation.addedNodes.forEach((newNode) => {
+ if (newNode.isConnected) _scanTree(newNode);
+ });
+ mutation.removedNodes.forEach((oldNode) => _unbindTree(oldNode));
+ });
+ }).observe(document.documentElement, { childList: true, subtree: true });
+ _scanTree(document.documentElement);
+ };
+ if (document.readyState !== "loading") init();
+ else document.addEventListener("DOMContentLoaded", init, true);
+ }
+ const __unsafeRefreshState = _scanTree;
+ exports2.$ = $;
+ exports2.$$ = $$;
+ exports2.Component = Component;
+ exports2.Hash = Hash;
+ exports2.LocalStorage = LocalStorage;
+ exports2.NewState = NewState;
+ exports2.SetTranslator = SetTranslator;
+ exports2.State = State;
+ exports2.Util = Util;
+ exports2.__unsafeRefreshState = __unsafeRefreshState;
+ Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
+});
diff --git a/test/minimal_perf.html b/test/minimal_perf.html
index 2fac90e..2fdee08 100644
--- a/test/minimal_perf.html
+++ b/test/minimal_perf.html
@@ -1,13 +1,13 @@
+
+
+
-