From 814c23a085c429f9b3b9d4166f11e1e77054285c Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Thu, 11 Jun 2026 19:48:40 +0800 Subject: [PATCH] chore: release v1.0.5 purifying architecture, eliminating __unsafeRefreshState, optimizing component rendering By: AICoder --- dist/datatable.js | 1623 +++++++++++++++++++++---------------- dist/datatable.min.js | 81 +- package-lock.json | 4 +- package.json | 2 +- src/DataTable.js | 737 +++++++++++++++++ src/index.js | 749 +---------------- src/perf.js | 43 - src/scroll.js | 46 -- src/selection.js | 242 ------ test/diagnostic.html | 26 +- test/index.html | 23 +- test/lib/base.js | 939 +++++++++++++++++++++ test/lib/datatable.js | 965 ++++++++++++++++++++++ test/lib/datatable.min.js | 1 + test/lib/state.js | 661 +++++++++++++++ test/minimal_perf.html | 11 +- vite.config.js | 21 +- 17 files changed, 4282 insertions(+), 1892 deletions(-) create mode 100644 src/DataTable.js delete mode 100644 src/perf.js delete mode 100644 src/scroll.js delete mode 100644 src/selection.js create mode 100644 test/lib/base.js create mode 100644 test/lib/datatable.js create mode 100644 test/lib/datatable.min.js create mode 100644 test/lib/state.js 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*/ + `
-
-
-
- -
-
-
- - - -
-
+
+
+
+ +
+
+
+ + + +
+
- + + + + + + + +
` -), 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(` - -
-
-
-
- -
-
-
- - - -
-
- - - - - -
-`),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 \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*/` +
+
+
+
+ +
+
+
+ + + +
+
+ + + + + + + + + + +
+`), 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*/` -
-
-
-
- -
-
-
- - - -
-
- - - - - - -
-`), 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">`); + 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*/ + ` + +` + ), 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*/ + ` +
+
+
+
+ +
+
+
+ + + +
+
+ + + + + + + + + + +
+` + ), 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 \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 @@ + + + -