2026-05-23 17:37:25 +08:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
const createScrollManager = (container, state, onRenderedListChange) => {
|
|
|
|
|
const vs = VirtualScroll({ itemHeight: 40 });
|
|
|
|
|
let scrollEl = null;
|
2026-05-17 17:49:48 +08:00
|
|
|
const refresh = () => {
|
2026-05-17 17:03:21 +08:00
|
|
|
if (!scrollEl) return;
|
|
|
|
|
const res = vs.calc(scrollEl, state.list);
|
2026-05-17 18:50:30 +08:00
|
|
|
if (res) {
|
2026-05-23 17:37:25 +08:00
|
|
|
if (state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-17 18:50:30 +08:00
|
|
|
Object.assign(state, {
|
|
|
|
|
prevHeight: res.prevHeight,
|
|
|
|
|
postHeight: res.postHeight,
|
|
|
|
|
_listStartIndex: res.listStartIndex,
|
2026-05-23 17:37:25 +08:00
|
|
|
_renderedList: res.renderedList
|
2026-05-17 18:50:30 +08:00
|
|
|
});
|
2026-05-23 17:37:25 +08:00
|
|
|
onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length);
|
2026-05-17 18:50:30 +08:00
|
|
|
}
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
return {
|
|
|
|
|
init: () => {
|
|
|
|
|
scrollEl = container.querySelector(".dt-main");
|
|
|
|
|
},
|
|
|
|
|
reset: (list) => {
|
|
|
|
|
state._listStartIndex = 0;
|
|
|
|
|
state._renderedList = vs.reset(list, scrollEl || container) || [];
|
|
|
|
|
if (state.list === list) {
|
|
|
|
|
vs.init(list, refresh);
|
2026-05-17 20:11:20 +08:00
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
},
|
|
|
|
|
refresh,
|
|
|
|
|
onScroll: refresh
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
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 = () => {
|
|
|
|
|
var _a;
|
|
|
|
|
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);
|
2026-05-17 20:11:20 +08:00
|
|
|
});
|
2026-05-23 17:37:25 +08:00
|
|
|
const hasSelection = boundMinRow !== Infinity;
|
|
|
|
|
if (!hasSelection && !lastHadSelection) return;
|
|
|
|
|
lastHadSelection = hasSelection;
|
|
|
|
|
const body = container.querySelector(".dt-body");
|
|
|
|
|
if (!body) return;
|
|
|
|
|
const rowNodes = body.children;
|
|
|
|
|
for (let r = 0; r < rowNodes.length; r++) {
|
|
|
|
|
const rowNode = rowNodes[r];
|
|
|
|
|
if (!rowNode.classList.contains("dt-body-row")) continue;
|
|
|
|
|
const absoluteRow = (((_a = rowNode._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex;
|
|
|
|
|
const cellNodes = rowNode.children;
|
|
|
|
|
if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
|
|
|
|
|
for (let i = 0; i < cellNodes.length; i++) {
|
|
|
|
|
if (cellNodes[i].classList.contains("dt-cell-selected")) {
|
|
|
|
|
cellNodes[i].classList.remove("dt-cell-selected");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < cellNodes.length; i++) {
|
|
|
|
|
const isSelected = isCellSelected(absoluteRow, i);
|
|
|
|
|
const hasClass = cellNodes[i].classList.contains("dt-cell-selected");
|
|
|
|
|
if (isSelected && !hasClass) {
|
|
|
|
|
cellNodes[i].classList.add("dt-cell-selected");
|
|
|
|
|
} else if (!isSelected && hasClass) {
|
|
|
|
|
cellNodes[i].classList.remove("dt-cell-selected");
|
2026-05-17 20:11:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
|
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
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) => {
|
2026-05-17 17:03:21 +08:00
|
|
|
if (!keepSelection) {
|
2026-05-23 17:37:25 +08:00
|
|
|
activeBounds = null;
|
|
|
|
|
startCell = null;
|
|
|
|
|
multiSelections = [];
|
|
|
|
|
applySelectionUI();
|
|
|
|
|
updateStatus();
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
|
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
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)
|
|
|
|
|
};
|
2026-05-17 17:03:21 +08:00
|
|
|
} else {
|
2026-05-23 17:37:25 +08:00
|
|
|
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);
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
startCell = { row, col };
|
|
|
|
|
activeBounds = { minRow: row, maxRow: row, minCol: col, maxCol: col };
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
|
|
|
|
state.isSelecting = true;
|
|
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
applySelectionUI();
|
|
|
|
|
updateStatus();
|
|
|
|
|
container.focus();
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
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;
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
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;
|
2026-05-17 18:50:30 +08:00
|
|
|
}
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-17 17:03:21 +08:00
|
|
|
});
|
2026-05-23 17:37:25 +08:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Paste Error:", err);
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
|
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
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();
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
return {
|
|
|
|
|
applySelectionUI,
|
|
|
|
|
clearAllActive,
|
|
|
|
|
startSelect,
|
|
|
|
|
updateSelect,
|
|
|
|
|
endSelect,
|
|
|
|
|
deleteSelected,
|
|
|
|
|
getSelectionBounds,
|
|
|
|
|
copy,
|
|
|
|
|
paste
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
const perf = createPerfMonitor();
|
|
|
|
|
state.perf = perf.stats;
|
|
|
|
|
const selection = createSelectionManager(container, state);
|
|
|
|
|
const scroll = createScrollManager(container, state, (renderedCount) => {
|
|
|
|
|
container.hideEditor();
|
|
|
|
|
selection.applySelectionUI();
|
|
|
|
|
});
|
|
|
|
|
let _prevSpacer, _postSpacer, _editorOverlay;
|
|
|
|
|
container.format = (val, field) => {
|
|
|
|
|
if (field.formatter) return field.formatter(val, field);
|
|
|
|
|
if (typeof val === "string") return val;
|
|
|
|
|
if (val === null || val === void 0) return "";
|
|
|
|
|
return typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
|
|
|
};
|
|
|
|
|
container.refreshNode = (node) => RefreshState(node);
|
|
|
|
|
container.refresh = () => {
|
|
|
|
|
const frameStart = perf.startFrame();
|
|
|
|
|
scroll.refresh();
|
|
|
|
|
if (!_prevSpacer) _prevSpacer = container.querySelector(".dt-spacer-prev");
|
|
|
|
|
if (_prevSpacer) {
|
|
|
|
|
const h = state.prevHeight || 0;
|
|
|
|
|
_prevSpacer.style.height = h + "px";
|
|
|
|
|
_prevSpacer.style.display = h > 0 ? "block" : "none";
|
|
|
|
|
}
|
|
|
|
|
if (!_postSpacer) _postSpacer = container.querySelector(".dt-spacer-post");
|
|
|
|
|
if (_postSpacer) {
|
|
|
|
|
const h = state.postHeight || 0;
|
|
|
|
|
_postSpacer.style.height = h + "px";
|
|
|
|
|
_postSpacer.style.display = h > 0 ? "block" : "none";
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
perf.endFrame(frameStart, state._renderedList.length);
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
container.onScroll = () => {
|
|
|
|
|
perf.onScroll();
|
|
|
|
|
container.refresh();
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
container.onMainMouseDown = (e) => {
|
|
|
|
|
var _a, _b;
|
|
|
|
|
const cell = e.target.closest(".dt-cell");
|
|
|
|
|
if (!cell) return;
|
|
|
|
|
const row = cell.closest(".dt-row");
|
|
|
|
|
if (!row || row.classList.contains("dt-header-row")) return;
|
|
|
|
|
const fIdx = ((_a = cell._ref) == null ? void 0 : _a.fIdx) ?? Array.from(row.children).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;
|
|
|
|
|
container.startSelect(absoluteRow, fIdx, e);
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
2026-05-23 17:37:25 +08:00
|
|
|
container.onMainMouseOver = (e) => {
|
|
|
|
|
var _a, _b;
|
|
|
|
|
if (!state.isSelecting) return;
|
|
|
|
|
const cell = e.target.closest(".dt-cell");
|
|
|
|
|
if (!cell) return;
|
|
|
|
|
const row = cell.closest(".dt-row");
|
|
|
|
|
if (!row || row.classList.contains("dt-header-row")) return;
|
|
|
|
|
const fIdx = ((_a = cell._ref) == null ? void 0 : _a.fIdx) ?? Array.from(row.children).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;
|
|
|
|
|
container.updateSelect(absoluteRow, fIdx);
|
|
|
|
|
};
|
|
|
|
|
container.onMainDblClick = (e) => {
|
|
|
|
|
var _a, _b;
|
|
|
|
|
const cell = e.target.closest(".dt-cell");
|
|
|
|
|
if (!cell) return;
|
|
|
|
|
const row = cell.closest(".dt-row");
|
|
|
|
|
if (!row || row.classList.contains("dt-header-row")) return;
|
|
|
|
|
const item = (_a = row._ref) == null ? void 0 : _a.item;
|
|
|
|
|
const field = (_b = cell._ref) == null ? void 0 : _b.f;
|
|
|
|
|
if (item && field) container.editCell(item, field, cell);
|
|
|
|
|
};
|
|
|
|
|
state.__watch("list", (list) => {
|
|
|
|
|
scroll.init();
|
|
|
|
|
scroll.reset(list);
|
|
|
|
|
});
|
|
|
|
|
state.__watch("fields", (fields) => {
|
|
|
|
|
if (!fields) return;
|
|
|
|
|
const gridTemplate = fields.map((f) => `var(--w-${f.id}, ${f.width || 150}px)`).join(" ");
|
|
|
|
|
container.style.setProperty("--dt-grid-template", gridTemplate);
|
|
|
|
|
});
|
|
|
|
|
let currentEditingNode = null;
|
|
|
|
|
container.editCell = (row, field, cellNode) => {
|
|
|
|
|
const overlay = container.querySelector(".dt-editor-overlay");
|
|
|
|
|
const editor = overlay.querySelector("AutoForm");
|
|
|
|
|
const rect = cellNode.getBoundingClientRect();
|
|
|
|
|
const rootRect = container.getBoundingClientRect();
|
|
|
|
|
currentEditingNode = cellNode;
|
|
|
|
|
let minW = rect.width;
|
|
|
|
|
if (field.type === "textarea" || field.type === "TagsInput") minW = Math.max(rect.width, 300);
|
|
|
|
|
else if (field.type === "radio") minW = Math.max(rect.width, 240);
|
|
|
|
|
overlay.style.display = "flex";
|
|
|
|
|
overlay.style.left = rect.left - rootRect.left + "px";
|
|
|
|
|
overlay.style.top = rect.top - rootRect.top + "px";
|
|
|
|
|
overlay.style.width = minW + "px";
|
|
|
|
|
overlay.style.height = "";
|
|
|
|
|
overlay.style.minHeight = "";
|
|
|
|
|
overlay.style.alignItems = "center";
|
|
|
|
|
if (field.type === "textarea" || field.type === "TagsInput") {
|
|
|
|
|
overlay.style.minHeight = (field.type === "textarea" ? 150 : rect.height) + "px";
|
|
|
|
|
overlay.style.height = "auto";
|
|
|
|
|
overlay.style.alignItems = "flex-start";
|
|
|
|
|
} else {
|
|
|
|
|
overlay.style.height = rect.height + "px";
|
|
|
|
|
}
|
|
|
|
|
State.editingSchema = [{ ...field, name: field.id, label: "" }];
|
|
|
|
|
State.editingData = row;
|
|
|
|
|
RefreshState(overlay);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const input = editor.querySelector(".form-control, .form-select, .form-check-input, input");
|
|
|
|
|
if (input) input.focus();
|
|
|
|
|
}, 30);
|
|
|
|
|
};
|
|
|
|
|
container.hideEditor = (save = true) => {
|
|
|
|
|
if (!_editorOverlay) _editorOverlay = container.querySelector(".dt-editor-overlay");
|
|
|
|
|
if (!_editorOverlay || _editorOverlay.style.display === "none") return;
|
|
|
|
|
_editorOverlay.style.display = "none";
|
|
|
|
|
if (save && currentEditingNode) {
|
|
|
|
|
RefreshState(currentEditingNode);
|
|
|
|
|
}
|
|
|
|
|
State.editingSchema = null;
|
|
|
|
|
State.editingData = null;
|
|
|
|
|
currentEditingNode = null;
|
|
|
|
|
container.focus();
|
|
|
|
|
};
|
|
|
|
|
container.startSelect = selection.startSelect;
|
|
|
|
|
container.updateSelect = selection.updateSelect;
|
|
|
|
|
container.deleteSelected = selection.deleteSelected;
|
2026-05-17 17:03:21 +08:00
|
|
|
container.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
2026-05-23 17:37:25 +08:00
|
|
|
const k = e.key.toLowerCase();
|
|
|
|
|
if (k === "c") {
|
2026-05-17 17:03:21 +08:00
|
|
|
e.preventDefault();
|
2026-05-23 17:37:25 +08:00
|
|
|
selection.copy();
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
if (k === "v") {
|
2026-05-17 17:03:21 +08:00
|
|
|
e.preventDefault();
|
2026-05-23 17:37:25 +08:00
|
|
|
selection.paste();
|
2026-05-17 17:03:21 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-23 17:37:25 +08:00
|
|
|
if (e.key === "Escape") container.hideEditor(false);
|
2026-05-17 17:03:21 +08:00
|
|
|
});
|
2026-05-23 17:37:25 +08:00
|
|
|
const onGlobalMouseDown = (e) => {
|
|
|
|
|
const overlay = container.querySelector(".dt-editor-overlay");
|
|
|
|
|
if (overlay && 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();
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener("mouseup", selection.endSelect);
|
2026-05-17 17:03:21 +08:00
|
|
|
document.addEventListener("mousedown", onGlobalMouseDown);
|
|
|
|
|
container._onUnload = () => {
|
|
|
|
|
document.removeEventListener("mousedown", onGlobalMouseDown);
|
2026-05-23 17:37:25 +08:00
|
|
|
window.removeEventListener("mouseup", selection.endSelect);
|
2026-05-17 17:03:21 +08:00
|
|
|
};
|
|
|
|
|
}, Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
2026-05-23 17:37:25 +08:00
|
|
|
<div class="dt-root d-flex flex-column h-100 border bg-body text-body overflow-hidden" style="position:relative; user-select:none; outline: none; min-height: 0" onunload="this._onUnload()" tabindex="0">
|
|
|
|
|
<div class="dt-main flex-grow-1 overflow-auto" $onscroll="this.onScroll()"
|
|
|
|
|
$onmousedown="this.onMainMouseDown(event)"
|
|
|
|
|
$onmouseover="this.onMainMouseOver(event)"
|
|
|
|
|
$ondblclick="this.onMainDblClick(event)"
|
|
|
|
|
style="overflow-anchor:none; min-height: 0">
|
|
|
|
|
<div class="dt-header border-bottom bg-light sticky-top" style="z-index:20">
|
|
|
|
|
<div class="dt-row dt-header-row fw-bold text-muted small">
|
|
|
|
|
<div $each="this.state?.fields || []" class="dt-cell border-end px-2 d-flex align-items-center">
|
|
|
|
|
<span $text="item.name" class="text-truncate"></span>
|
2026-05-17 17:03:21 +08:00
|
|
|
</div>
|
2026-05-23 17:37:25 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="dt-body" style="position:relative">
|
|
|
|
|
<div class="dt-spacer-prev flex-shrink-0" style="display:none"></div>
|
|
|
|
|
<div $each="this.state?._renderedList || []" key="id" index="rIdx" class="dt-row dt-body-row border-bottom bg-white">
|
|
|
|
|
<div $each="this.state?.fields || []" as="f" index="fIdx" class="dt-cell border-end px-2 d-flex align-items-center">
|
|
|
|
|
<span $text="this.format(item[f.id], f)" class="text-truncate"></span>
|
2026-05-17 17:03:21 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-23 17:37:25 +08:00
|
|
|
<div class="dt-spacer-post flex-shrink-0" style="display:none"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Shared Editor Overlay -->
|
|
|
|
|
<div class="dt-editor-overlay" style="display: none" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
|
|
|
|
<AutoForm inline class="h-100 w-100" $onsubmit="thisNode.closest('DataTable').hideEditor(true)"/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="dt-footer border-top bg-light d-flex align-items-center px-2 py-1 small text-muted" style="height:32px">
|
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
Total: <span $text="this.state?.list?.length || 0"></span> |
|
|
|
|
|
Selected Rows: <span $text="this.state?.selectedRowCount || 0"></span>
|
2026-05-17 17:03:21 +08:00
|
|
|
</div>
|
2026-05-23 17:37:25 +08:00
|
|
|
<button class="btn btn-xs btn-outline-danger py-0 px-2" style="font-size: 11px" $disabled="!this.state?.selectedRowCount" $onclick="this.deleteSelected()">
|
|
|
|
|
<i class="bi bi-trash me-1"></i>Delete Selected
|
|
|
|
|
</button>
|
2026-05-17 17:03:21 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`
|
2026-05-23 17:37:25 +08:00
|
|
|
), Util.makeDom(
|
|
|
|
|
/*html*/
|
|
|
|
|
`
|
|
|
|
|
<style>
|
|
|
|
|
DataTable { display: block; }
|
|
|
|
|
.dt-root { font-size: 0.875rem; }
|
|
|
|
|
.dt-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: var(--dt-grid-template);
|
|
|
|
|
width: max-content;
|
|
|
|
|
min-width: 100%;
|
|
|
|
|
height: 40px;
|
|
|
|
|
}
|
|
|
|
|
.dt-header-row {
|
|
|
|
|
background-color: var(--bs-tertiary-bg);
|
|
|
|
|
}
|
|
|
|
|
.dt-cell {
|
|
|
|
|
background: inherit;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.dt-body-row:hover {
|
|
|
|
|
background-color: var(--bs-secondary-bg) !important;
|
|
|
|
|
}
|
|
|
|
|
.dt-cell-selected {
|
|
|
|
|
background-color: var(--bs-primary-bg-subtle) !important;
|
|
|
|
|
}
|
|
|
|
|
.dt-editor-overlay {
|
|
|
|
|
position: absolute;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
background: var(--bs-body-bg);
|
|
|
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
|
|
|
|
border: 1px solid var(--bs-primary);
|
|
|
|
|
align-items: center;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
transition: top 0.1s ease, left 0.1s ease, width 0.1s ease, height 0.1s ease, min-height 0.1s ease;
|
|
|
|
|
}
|
|
|
|
|
.dt-editor-overlay .auto-form-root, .dt-editor-overlay form {
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
.dt-editor-overlay .form-control, .dt-editor-overlay .form-select {
|
|
|
|
|
border: none !important;
|
|
|
|
|
border-radius: 0 !important;
|
|
|
|
|
height: 100% !important;
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
box-shadow: none !important;
|
|
|
|
|
padding: 0 8px !important;
|
|
|
|
|
}
|
|
|
|
|
.dt-editor-overlay .form-check.form-switch {
|
|
|
|
|
padding-left: 2.5em;
|
|
|
|
|
margin: 0;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
.btn-xs {
|
|
|
|
|
padding: 1px 5px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
`
|
2026-05-17 17:03:21 +08:00
|
|
|
));
|
2026-05-17 20:11:20 +08:00
|
|
|
if (typeof document !== "undefined") RefreshState(document.documentElement);
|