perf: implement AOT template flattening for columns
This commit is contained in:
parent
59a9e5d650
commit
6081140907
375
dist/datatable.js
vendored
375
dist/datatable.js
vendored
@ -50,11 +50,11 @@ const createPerfMonitor = () => {
|
|||||||
const createScrollManager = (container, state, onRenderedListChange) => {
|
const createScrollManager = (container, state, onRenderedListChange) => {
|
||||||
const vs = VirtualScroll({ itemHeight: 40 });
|
const vs = VirtualScroll({ itemHeight: 40 });
|
||||||
let scrollEl = null;
|
let scrollEl = null;
|
||||||
const refresh = () => {
|
const refresh = (isLayoutChange = false) => {
|
||||||
if (!scrollEl) return;
|
if (!scrollEl) return;
|
||||||
const res = vs.calc(scrollEl, state.list);
|
const res = vs.calc(scrollEl, state.list);
|
||||||
if (res) {
|
if (res) {
|
||||||
if (state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) {
|
if (!isLayoutChange && state.prevHeight === res.prevHeight && state.postHeight === res.postHeight && state._listStartIndex === res.listStartIndex && state._renderedList.length === res.renderedList.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
@ -63,7 +63,7 @@ const createScrollManager = (container, state, onRenderedListChange) => {
|
|||||||
_listStartIndex: res.listStartIndex,
|
_listStartIndex: res.listStartIndex,
|
||||||
_renderedList: res.renderedList
|
_renderedList: res.renderedList
|
||||||
});
|
});
|
||||||
onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length);
|
onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length, isLayoutChange);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
@ -72,13 +72,13 @@ const createScrollManager = (container, state, onRenderedListChange) => {
|
|||||||
},
|
},
|
||||||
reset: (list) => {
|
reset: (list) => {
|
||||||
state._listStartIndex = 0;
|
state._listStartIndex = 0;
|
||||||
state._renderedList = vs.reset(list, scrollEl || container) || [];
|
vs.reset(list, scrollEl || container);
|
||||||
if (state.list === list) {
|
if (state.list === list) {
|
||||||
vs.init(list, refresh);
|
vs.init(list, () => refresh(true));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh,
|
refresh,
|
||||||
onScroll: refresh
|
onScroll: () => refresh(false)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const createSelectionManager = (container, state) => {
|
const createSelectionManager = (container, state) => {
|
||||||
@ -92,6 +92,7 @@ const createSelectionManager = (container, state) => {
|
|||||||
let lastHadSelection = false;
|
let lastHadSelection = false;
|
||||||
const applySelectionUI = () => {
|
const applySelectionUI = () => {
|
||||||
var _a;
|
var _a;
|
||||||
|
if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return;
|
||||||
let boundMinRow = Infinity, boundMaxRow = -Infinity;
|
let boundMinRow = Infinity, boundMaxRow = -Infinity;
|
||||||
if (activeBounds) {
|
if (activeBounds) {
|
||||||
boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
|
boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
|
||||||
@ -106,28 +107,24 @@ const createSelectionManager = (container, state) => {
|
|||||||
lastHadSelection = hasSelection;
|
lastHadSelection = hasSelection;
|
||||||
const body = container.querySelector(".dt-body");
|
const body = container.querySelector(".dt-body");
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
const rowNodes = body.children;
|
const nodes = body.children;
|
||||||
for (let r = 0; r < rowNodes.length; r++) {
|
for (let r = 0; r < nodes.length; r++) {
|
||||||
const rowNode = rowNodes[r];
|
const rowNode = nodes[r];
|
||||||
if (!rowNode.classList.contains("dt-body-row")) continue;
|
if (!rowNode.classList.contains("dt-body-row")) continue;
|
||||||
const absoluteRow = (((_a = rowNode._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex;
|
const absoluteRow = (((_a = rowNode._ref) == null ? void 0 : _a.rIdx) ?? -1) + state._listStartIndex;
|
||||||
const cellNodes = rowNode.children;
|
|
||||||
if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
|
if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
|
||||||
for (let i = 0; i < cellNodes.length; i++) {
|
const cellNodes2 = rowNode.children;
|
||||||
if (cellNodes[i].classList.contains("dt-cell-selected")) {
|
for (let i = 0; i < cellNodes2.length; i++) {
|
||||||
cellNodes[i].classList.remove("dt-cell-selected");
|
cellNodes2[i].classList.remove("dt-cell-selected");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const cellNodes = rowNode.children;
|
||||||
for (let i = 0; i < cellNodes.length; i++) {
|
for (let i = 0; i < cellNodes.length; i++) {
|
||||||
const isSelected = isCellSelected(absoluteRow, i);
|
const isSelected = isCellSelected(absoluteRow, i);
|
||||||
const hasClass = cellNodes[i].classList.contains("dt-cell-selected");
|
const cell = cellNodes[i];
|
||||||
if (isSelected && !hasClass) {
|
if (isSelected) cell.classList.add("dt-cell-selected");
|
||||||
cellNodes[i].classList.add("dt-cell-selected");
|
else cell.classList.remove("dt-cell-selected");
|
||||||
} else if (!isSelected && hasClass) {
|
|
||||||
cellNodes[i].classList.remove("dt-cell-selected");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -312,13 +309,20 @@ Component.register("DataTable", (container) => {
|
|||||||
prevHeight: 0,
|
prevHeight: 0,
|
||||||
postHeight: 0,
|
postHeight: 0,
|
||||||
_listStartIndex: 0,
|
_listStartIndex: 0,
|
||||||
selectedRowCount: 0
|
selectedRowCount: 0,
|
||||||
|
_originalList: [],
|
||||||
|
sortConfig: { fieldId: null, direction: null },
|
||||||
|
filterConfig: {},
|
||||||
|
activeFieldId: null,
|
||||||
|
_internalUpdate: false,
|
||||||
|
_appliedHash: "",
|
||||||
|
_fieldsDirty: false,
|
||||||
|
_masterCellNodes: null
|
||||||
});
|
});
|
||||||
const perf = createPerfMonitor();
|
const perf = createPerfMonitor();
|
||||||
state.perf = perf.stats;
|
state.perf = perf.stats;
|
||||||
const selection = createSelectionManager(container, state);
|
const selection = createSelectionManager(container, state);
|
||||||
const scroll = createScrollManager(container, state, (renderedCount) => {
|
const scroll = createScrollManager(container, state, (renderedCount, isLayoutChange) => {
|
||||||
container.hideEditor();
|
|
||||||
selection.applySelectionUI();
|
selection.applySelectionUI();
|
||||||
});
|
});
|
||||||
let _prevSpacer, _postSpacer, _editorOverlay;
|
let _prevSpacer, _postSpacer, _editorOverlay;
|
||||||
@ -346,9 +350,119 @@ Component.register("DataTable", (container) => {
|
|||||||
}
|
}
|
||||||
perf.endFrame(frameStart, state._renderedList.length);
|
perf.endFrame(frameStart, state._renderedList.length);
|
||||||
};
|
};
|
||||||
|
let _scrollRaf = null;
|
||||||
container.onScroll = () => {
|
container.onScroll = () => {
|
||||||
perf.onScroll();
|
perf.onScroll();
|
||||||
container.refresh();
|
container.refresh();
|
||||||
|
if (_scrollRaf) return;
|
||||||
|
_scrollRaf = requestAnimationFrame(() => {
|
||||||
|
_scrollRaf = null;
|
||||||
|
const menu = container.querySelector(".dt-column-menu");
|
||||||
|
if (menu && menu.style.display !== "none" && state.activeFieldId) {
|
||||||
|
const headerCell = container.querySelector(`.header-cell[data-id="${state.activeFieldId}"]`);
|
||||||
|
if (headerCell) {
|
||||||
|
const btn = headerCell.querySelector(".header-menu-btn");
|
||||||
|
const btnRect = btn.getBoundingClientRect();
|
||||||
|
const rootRect = container.getBoundingClientRect();
|
||||||
|
menu.style.left = btnRect.right - rootRect.left - 180 + "px";
|
||||||
|
menu.style.top = btnRect.bottom - rootRect.top + 5 + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
const startTime = performance.now();
|
||||||
|
let list = options.force ? state.list : [...state._originalList || []];
|
||||||
|
if (!options.force) {
|
||||||
|
Object.keys(targetFilters).forEach((fieldId) => {
|
||||||
|
const val = targetFilters[fieldId];
|
||||||
|
if (val) {
|
||||||
|
const lowerVal = String(val).toLowerCase();
|
||||||
|
list = list.filter((item) => String(item[fieldId] ?? "").toLowerCase().includes(lowerVal));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (targetSort && targetSort.fieldId && targetSort.direction) {
|
||||||
|
list.sort((a, b) => {
|
||||||
|
let va = a[targetSort.fieldId], vb = b[targetSort.fieldId];
|
||||||
|
if (va === vb) return 0;
|
||||||
|
if (va === null || va === void 0) return 1;
|
||||||
|
if (vb === null || vb === void 0) return -1;
|
||||||
|
const res = va > vb ? 1 : -1;
|
||||||
|
return targetSort.direction === "asc" ? res : -res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.__perfTrace = { evalCount: 0, evalTotal: 0 };
|
||||||
|
performance.now();
|
||||||
|
state._appliedHash = currentHash;
|
||||||
|
if (options.sort !== void 0) state.sortConfig = targetSort;
|
||||||
|
state.list = list;
|
||||||
|
state._internalUpdate = false;
|
||||||
|
const frameEnd = performance.now();
|
||||||
|
const totalTime = frameEnd - startTime;
|
||||||
|
console.log(`[DataTable Performance Profile] Sync Block: ${totalTime.toFixed(2)}ms (Eval: ${window.__perfTrace.evalCount})`);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`[DataTable Performance Profile] E2E Paint: ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.__perfTrace = null;
|
||||||
|
};
|
||||||
|
container.showColumnMenu = (field, e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const btn = e.currentTarget;
|
||||||
|
const rect = btn.getBoundingClientRect();
|
||||||
|
const rootRect = container.getBoundingClientRect();
|
||||||
|
const menu = container.querySelector(".dt-column-menu");
|
||||||
|
state.activeFieldId = field.id;
|
||||||
|
menu.style.display = "block";
|
||||||
|
menu.style.left = rect.right - rootRect.left - 180 + "px";
|
||||||
|
menu.style.top = rect.bottom - rootRect.top + 5 + "px";
|
||||||
|
const onGlobalClick = (ev) => {
|
||||||
|
if (menu.contains(ev.target)) return;
|
||||||
|
if (!btn.contains(ev.target)) {
|
||||||
|
container.hideColumnMenu();
|
||||||
|
container.applySortFilter();
|
||||||
|
document.removeEventListener("mousedown", onGlobalClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", onGlobalClick);
|
||||||
|
RefreshState(menu);
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = menu.querySelector("input");
|
||||||
|
if (input) input.focus();
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
container.hideColumnMenu = () => {
|
||||||
|
const menu = container.querySelector(".dt-column-menu");
|
||||||
|
if (menu) menu.style.display = "none";
|
||||||
|
};
|
||||||
|
container.setSort = (direction) => {
|
||||||
|
container.applySortFilter({ sort: direction });
|
||||||
|
};
|
||||||
|
container.clearColumnSettings = () => {
|
||||||
|
const filters = { ...state.filterConfig };
|
||||||
|
delete filters[state.activeFieldId];
|
||||||
|
state.filterConfig = filters;
|
||||||
|
container.applySortFilter({ sort: null });
|
||||||
|
container.hideColumnMenu();
|
||||||
|
};
|
||||||
|
container._initRow = (rowNode) => {
|
||||||
|
if (!rowNode || !rowNode.children) return;
|
||||||
|
const cells = rowNode.children;
|
||||||
|
for (let i = 0; i < cells.length; i++) {
|
||||||
|
const cell = cells[i];
|
||||||
|
const fIdx = parseInt(cell.dataset.fidx);
|
||||||
|
if (!isNaN(fIdx)) {
|
||||||
|
cell._refExt = { f: state.fields[fIdx], fIdx };
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
container.onMainMouseDown = (e) => {
|
container.onMainMouseDown = (e) => {
|
||||||
var _a, _b;
|
var _a, _b;
|
||||||
@ -356,10 +470,9 @@ Component.register("DataTable", (container) => {
|
|||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
const row = cell.closest(".dt-row");
|
const row = cell.closest(".dt-row");
|
||||||
if (!row || row.classList.contains("dt-header-row")) return;
|
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 fIdx = cell.dataset.fidx !== void 0 ? parseInt(cell.dataset.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 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(rIdx + state._listStartIndex, fIdx, e);
|
||||||
container.startSelect(absoluteRow, fIdx, e);
|
|
||||||
};
|
};
|
||||||
container.onMainMouseOver = (e) => {
|
container.onMainMouseOver = (e) => {
|
||||||
var _a, _b;
|
var _a, _b;
|
||||||
@ -368,29 +481,57 @@ Component.register("DataTable", (container) => {
|
|||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
const row = cell.closest(".dt-row");
|
const row = cell.closest(".dt-row");
|
||||||
if (!row || row.classList.contains("dt-header-row")) return;
|
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 fIdx = cell.dataset.fidx !== void 0 ? parseInt(cell.dataset.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 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(rIdx + state._listStartIndex, fIdx);
|
||||||
container.updateSelect(absoluteRow, fIdx);
|
|
||||||
};
|
};
|
||||||
container.onMainDblClick = (e) => {
|
container.onMainDblClick = (e) => {
|
||||||
var _a, _b;
|
var _a, _b, _c;
|
||||||
const cell = e.target.closest(".dt-cell");
|
const cell = e.target.closest(".dt-cell");
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
const row = cell.closest(".dt-row");
|
const row = cell.closest(".dt-row");
|
||||||
if (!row || row.classList.contains("dt-header-row")) return;
|
if (!row || row.classList.contains("dt-header-row")) return;
|
||||||
const item = (_a = row._ref) == null ? void 0 : _a.item;
|
const item = (_a = row._ref) == null ? void 0 : _a.item;
|
||||||
const field = (_b = cell._ref) == null ? void 0 : _b.f;
|
const fIdx = cell.dataset.fidx !== void 0 ? parseInt(cell.dataset.fidx) : ((_b = cell._ref) == null ? void 0 : _b.fIdx) ?? Array.from(row.children).indexOf(cell);
|
||||||
|
const field = ((_c = cell._ref) == null ? void 0 : _c.f) ?? state.fields[fIdx];
|
||||||
if (item && field) container.editCell(item, field, cell);
|
if (item && field) container.editCell(item, field, cell);
|
||||||
};
|
};
|
||||||
state.__watch("list", (list) => {
|
|
||||||
scroll.init();
|
|
||||||
scroll.reset(list);
|
|
||||||
});
|
|
||||||
state.__watch("fields", (fields) => {
|
state.__watch("fields", (fields) => {
|
||||||
if (!fields) return;
|
if (!fields) return;
|
||||||
|
state._fieldsDirty = true;
|
||||||
const gridTemplate = fields.map((f) => `var(--w-${f.id}, ${f.width || 150}px)`).join(" ");
|
const gridTemplate = fields.map((f) => `var(--w-${f.id}, ${f.width || 150}px)`).join(" ");
|
||||||
|
const totalWidth = fields.reduce((sum, f) => sum + (f.width || 150), 0);
|
||||||
container.style.setProperty("--dt-grid-template", gridTemplate);
|
container.style.setProperty("--dt-grid-template", gridTemplate);
|
||||||
|
container.style.setProperty("--dt-row-width", totalWidth + "px");
|
||||||
|
});
|
||||||
|
state.__watch("list", (list) => {
|
||||||
|
if (state._fieldsDirty) {
|
||||||
|
state._fieldsDirty = false;
|
||||||
|
const rowTemplate = container.querySelector('.dt-body template[index="rIdx"]');
|
||||||
|
if (rowTemplate) {
|
||||||
|
const fieldTemplate = rowTemplate.content.querySelector('template[as="f"]');
|
||||||
|
if (fieldTemplate) {
|
||||||
|
if (!state._masterCellNodes) {
|
||||||
|
state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map((n) => n.cloneNode(true));
|
||||||
|
}
|
||||||
|
fieldTemplate.removeAttribute("$each");
|
||||||
|
fieldTemplate.removeAttribute("as");
|
||||||
|
fieldTemplate.removeAttribute("index");
|
||||||
|
fieldTemplate.setAttribute("$if", "true");
|
||||||
|
fieldTemplate.content.textContent = "";
|
||||||
|
state.fields.forEach((f, fIdx) => {
|
||||||
|
state._masterCellNodes.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 || []];
|
||||||
|
scroll.init();
|
||||||
|
scroll.reset(list);
|
||||||
});
|
});
|
||||||
let currentEditingNode = null;
|
let currentEditingNode = null;
|
||||||
container.editCell = (row, field, cellNode) => {
|
container.editCell = (row, field, cellNode) => {
|
||||||
@ -406,16 +547,7 @@ Component.register("DataTable", (container) => {
|
|||||||
overlay.style.left = rect.left - rootRect.left + "px";
|
overlay.style.left = rect.left - rootRect.left + "px";
|
||||||
overlay.style.top = rect.top - rootRect.top + "px";
|
overlay.style.top = rect.top - rootRect.top + "px";
|
||||||
overlay.style.width = minW + "px";
|
overlay.style.width = minW + "px";
|
||||||
overlay.style.height = "";
|
overlay.style.height = field.type === "textarea" || field.type === "TagsInput" ? "auto" : rect.height + "px";
|
||||||
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.editingSchema = [{ ...field, name: field.id, label: "" }];
|
||||||
State.editingData = row;
|
State.editingData = row;
|
||||||
RefreshState(overlay);
|
RefreshState(overlay);
|
||||||
@ -428,36 +560,16 @@ Component.register("DataTable", (container) => {
|
|||||||
if (!_editorOverlay) _editorOverlay = container.querySelector(".dt-editor-overlay");
|
if (!_editorOverlay) _editorOverlay = container.querySelector(".dt-editor-overlay");
|
||||||
if (!_editorOverlay || _editorOverlay.style.display === "none") return;
|
if (!_editorOverlay || _editorOverlay.style.display === "none") return;
|
||||||
_editorOverlay.style.display = "none";
|
_editorOverlay.style.display = "none";
|
||||||
if (save && currentEditingNode) {
|
if (save && currentEditingNode) RefreshState(currentEditingNode);
|
||||||
RefreshState(currentEditingNode);
|
State.editingSchema = State.editingData = currentEditingNode = null;
|
||||||
}
|
|
||||||
State.editingSchema = null;
|
|
||||||
State.editingData = null;
|
|
||||||
currentEditingNode = null;
|
|
||||||
container.focus();
|
container.focus();
|
||||||
};
|
};
|
||||||
container.startSelect = selection.startSelect;
|
container.startSelect = selection.startSelect;
|
||||||
container.updateSelect = selection.updateSelect;
|
container.updateSelect = selection.updateSelect;
|
||||||
container.deleteSelected = selection.deleteSelected;
|
container.deleteSelected = selection.deleteSelected;
|
||||||
container.addEventListener("keydown", (e) => {
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
const k = e.key.toLowerCase();
|
|
||||||
if (k === "c") {
|
|
||||||
e.preventDefault();
|
|
||||||
selection.copy();
|
|
||||||
}
|
|
||||||
if (k === "v") {
|
|
||||||
e.preventDefault();
|
|
||||||
selection.paste();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") container.hideEditor(false);
|
|
||||||
});
|
|
||||||
const onGlobalMouseDown = (e) => {
|
const onGlobalMouseDown = (e) => {
|
||||||
const overlay = container.querySelector(".dt-editor-overlay");
|
const overlay = container.querySelector(".dt-editor-overlay");
|
||||||
if (overlay && overlay.style.display !== "none" && !overlay.contains(e.target)) {
|
if (overlay && overlay.style.display !== "none" && !overlay.contains(e.target)) container.hideEditor(true);
|
||||||
container.hideEditor(true);
|
|
||||||
}
|
|
||||||
if (!container.contains(e.target) && !(overlay == null ? void 0 : overlay.contains(e.target))) selection.clearAllActive();
|
if (!container.contains(e.target) && !(overlay == null ? void 0 : overlay.contains(e.target))) selection.clearAllActive();
|
||||||
};
|
};
|
||||||
window.addEventListener("mouseup", selection.endSelect);
|
window.addEventListener("mouseup", selection.endSelect);
|
||||||
@ -477,35 +589,70 @@ Component.register("DataTable", (container) => {
|
|||||||
style="overflow-anchor:none; min-height: 0">
|
style="overflow-anchor:none; min-height: 0">
|
||||||
<div class="dt-header border-bottom bg-light sticky-top" style="z-index:20">
|
<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 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">
|
<template $each="this.state?.fields || []">
|
||||||
<span $text="item.name" class="text-truncate"></span>
|
<div $data-id="item.id" class="dt-cell border-end px-2 d-flex align-items-center justify-content-between header-cell">
|
||||||
</div>
|
<div class="d-flex align-items-center overflow-hidden">
|
||||||
|
<i $if="this.state?.sortConfig?.fieldId === item.id"
|
||||||
|
$class="'bi bi-sort-' + (this.state?.sortConfig?.direction === 'asc' ? 'down' : 'up-alt') + ' me-1 text-primary'"></i>
|
||||||
|
<i $if="this.state?.filterConfig?.[item.id]" class="bi bi-filter me-1 text-primary"></i>
|
||||||
|
<span $text="item.name" class="text-truncate"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-xs btn-link text-muted p-0 border-0 header-menu-btn" $onclick="this.showColumnMenu(item, event)">
|
||||||
|
<i class="bi bi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dt-body" style="position:relative">
|
<div class="dt-body" style="position:relative">
|
||||||
<div class="dt-spacer-prev flex-shrink-0" style="display:none"></div>
|
<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">
|
<template $each="this.state?._renderedList || []" key="id" index="rIdx">
|
||||||
<div $each="this.state?.fields || []" as="f" index="fIdx" class="dt-cell border-end px-2 d-flex align-items-center">
|
<div class="dt-row dt-body-row border-bottom bg-white" $.="this._initRow(thisNode)">
|
||||||
<span $text="this.format(item[f.id], f)" class="text-truncate"></span>
|
<template $each="this.state?.fields || []" as="f" index="fIdx">
|
||||||
|
<div class="dt-cell border-end px-2 d-flex align-items-center">
|
||||||
|
<span $text="!f.formatter ? item[f.id] : this.format(item[f.id], f)" class="text-truncate"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="dt-spacer-post flex-shrink-0" style="display:none"></div>
|
<div class="dt-spacer-post flex-shrink-0" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Shared Editor Overlay -->
|
<div class="dt-column-menu border bg-body shadow-sm rounded" style="display:none; position:absolute; z-index:2000; min-width:180px" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
||||||
<div class="dt-editor-overlay" style="display: none" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
<div class="list-group list-group-flush">
|
||||||
|
<button class="list-group-item list-group-item-action py-2 d-flex align-items-center" $onclick="this.setSort('asc')">
|
||||||
|
<i class="bi bi-sort-alpha-down me-2"></i> Sort Ascending
|
||||||
|
</button>
|
||||||
|
<button class="list-group-item list-group-item-action py-2 d-flex align-items-center" $onclick="this.setSort('desc')">
|
||||||
|
<i class="bi bi-sort-alpha-up-alt me-2"></i> Sort Descending
|
||||||
|
</button>
|
||||||
|
<div class="p-2 border-top bg-light-subtle">
|
||||||
|
<div class="d-flex align-items-center mb-1 text-muted fw-bold" style="font-size: 10px">
|
||||||
|
<i class="bi bi-filter me-1"></i> LOCAL FILTER
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control form-control-sm" placeholder="Search..."
|
||||||
|
$bind="this.state.filterConfig[this.state.activeFieldId]"
|
||||||
|
$onkeydown="if(event.key === 'Enter') this.applySortFilter()">
|
||||||
|
</div>
|
||||||
|
<button class="list-group-item list-group-item-action py-2 text-danger d-flex align-items-center border-top" $onclick="this.clearColumnSettings()">
|
||||||
|
<i class="bi bi-x-circle me-2"></i> Clear Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dt-editor-overlay dt-editor-container" style="display: none" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
||||||
<AutoForm inline class="h-100 w-100" $onsubmit="thisNode.closest('DataTable').hideEditor(true)"/>
|
<AutoForm inline class="h-100 w-100" $onsubmit="thisNode.closest('DataTable').hideEditor(true)"/>
|
||||||
</div>
|
</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="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">
|
<div class="d-flex align-items-center gap-2">
|
||||||
Total: <span $text="this.state?.list?.length || 0"></span> |
|
<span $text="(this.state?.selectedRowCount || 0) + ' / ' + (this.state?.list?.length || 0)"></span>
|
||||||
Selected Rows: <span $text="this.state?.selectedRowCount || 0"></span>
|
<button class="btn btn-xs btn-link text-danger p-0 border-0" title="Delete Selected"
|
||||||
|
$if="this.state?.selectedRowCount" $onclick="this.deleteSelected()">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@ -518,57 +665,27 @@ Component.register("DataTable", (container) => {
|
|||||||
.dt-row {
|
.dt-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--dt-grid-template);
|
grid-template-columns: var(--dt-grid-template);
|
||||||
width: max-content;
|
width: var(--dt-row-width, max-content);
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
contain: paint layout;
|
||||||
}
|
}
|
||||||
.dt-header-row {
|
.dt-header-row { background-color: var(--bs-tertiary-bg); }
|
||||||
background-color: var(--bs-tertiary-bg);
|
.dt-cell { background: inherit; white-space: nowrap; flex-shrink: 0; contain: content; }
|
||||||
}
|
.dt-body-row:hover { background-color: var(--bs-secondary-bg) !important; }
|
||||||
.dt-cell {
|
.dt-cell-selected { background-color: var(--bs-primary-bg-subtle) !important; }
|
||||||
background: inherit;
|
.header-cell .header-menu-btn { opacity: 0; transition: opacity 0.2s; }
|
||||||
white-space: nowrap;
|
.header-cell:hover .header-menu-btn { opacity: 1; }
|
||||||
flex-shrink: 0;
|
.dt-column-menu { box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; }
|
||||||
}
|
.dt-column-menu .list-group-item { border: none; cursor: pointer; }
|
||||||
.dt-body-row:hover {
|
.dt-column-menu .list-group-item:hover { background-color: var(--bs-light); }
|
||||||
background-color: var(--bs-secondary-bg) !important;
|
|
||||||
}
|
|
||||||
.dt-cell-selected {
|
|
||||||
background-color: var(--bs-primary-bg-subtle) !important;
|
|
||||||
}
|
|
||||||
.dt-editor-overlay {
|
.dt-editor-overlay {
|
||||||
position: absolute;
|
position: absolute; z-index: 1000; background: var(--bs-body-bg);
|
||||||
z-index: 1000;
|
box-shadow: 0 4px 16px rgba(0,0,0,0.25); border: 1px solid var(--bs-primary);
|
||||||
background: var(--bs-body-bg);
|
align-items: center; overflow: visible;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
.dt-editor-overlay .form-control { border: none !important; border-radius: 0 !important; height: 100% !important; padding: 0 8px !important; }
|
||||||
|
.btn-xs { padding: 1px 5px; line-height: 1.5; }
|
||||||
</style>
|
</style>
|
||||||
`
|
`
|
||||||
));
|
));
|
||||||
|
|||||||
2
dist/datatable.min.js
vendored
2
dist/datatable.min.js
vendored
File diff suppressed because one or more lines are too long
387
src/index.js
387
src/index.js
@ -11,7 +11,15 @@ Component.register('DataTable', container => {
|
|||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
list: [], fields: [], _renderedList: [],
|
list: [], fields: [], _renderedList: [],
|
||||||
prevHeight: 0, postHeight: 0, _listStartIndex: 0,
|
prevHeight: 0, postHeight: 0, _listStartIndex: 0,
|
||||||
selectedRowCount: 0
|
selectedRowCount: 0,
|
||||||
|
_originalList: [],
|
||||||
|
sortConfig: { fieldId: null, direction: null },
|
||||||
|
filterConfig: {},
|
||||||
|
activeFieldId: null,
|
||||||
|
_internalUpdate: false,
|
||||||
|
_appliedHash: '',
|
||||||
|
_fieldsDirty: false,
|
||||||
|
_masterCellNodes: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const perf = createPerfMonitor();
|
const perf = createPerfMonitor();
|
||||||
@ -19,10 +27,7 @@ Component.register('DataTable', container => {
|
|||||||
|
|
||||||
const selection = createSelectionManager(container, state);
|
const selection = createSelectionManager(container, state);
|
||||||
|
|
||||||
const scroll = createScrollManager(container, state, (renderedCount) => {
|
const scroll = createScrollManager(container, state, (renderedCount, isLayoutChange) => {
|
||||||
// Only hide editor if we actually scroll past its visibility range
|
|
||||||
// For now, simple hide for safety
|
|
||||||
container.hideEditor();
|
|
||||||
selection.applySelectionUI();
|
selection.applySelectionUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,7 +46,6 @@ Component.register('DataTable', container => {
|
|||||||
const frameStart = perf.startFrame();
|
const frameStart = perf.startFrame();
|
||||||
scroll.refresh();
|
scroll.refresh();
|
||||||
|
|
||||||
// Manual DOM Sync for Spacers (Authorized optimization to avoid $if/$style overhead)
|
|
||||||
if (!_prevSpacer) _prevSpacer = container.querySelector('.dt-spacer-prev');
|
if (!_prevSpacer) _prevSpacer = container.querySelector('.dt-spacer-prev');
|
||||||
if (_prevSpacer) {
|
if (_prevSpacer) {
|
||||||
const h = state.prevHeight || 0;
|
const h = state.prevHeight || 0;
|
||||||
@ -58,22 +62,143 @@ Component.register('DataTable', container => {
|
|||||||
perf.endFrame(frameStart, state._renderedList.length);
|
perf.endFrame(frameStart, state._renderedList.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let _scrollRaf = null;
|
||||||
container.onScroll = () => {
|
container.onScroll = () => {
|
||||||
perf.onScroll();
|
perf.onScroll();
|
||||||
container.refresh();
|
container.refresh();
|
||||||
|
|
||||||
|
if (_scrollRaf) return;
|
||||||
|
_scrollRaf = requestAnimationFrame(() => {
|
||||||
|
_scrollRaf = null;
|
||||||
|
const menu = container.querySelector('.dt-column-menu');
|
||||||
|
if (menu && menu.style.display !== 'none' && state.activeFieldId) {
|
||||||
|
const headerCell = container.querySelector(`.header-cell[data-id="${state.activeFieldId}"]`);
|
||||||
|
if (headerCell) {
|
||||||
|
const btn = headerCell.querySelector('.header-menu-btn');
|
||||||
|
const btnRect = btn.getBoundingClientRect();
|
||||||
|
const rootRect = container.getBoundingClientRect();
|
||||||
|
menu.style.left = (btnRect.right - rootRect.left - 180) + 'px';
|
||||||
|
menu.style.top = (btnRect.bottom - rootRect.top + 5) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
const startTime = performance.now();
|
||||||
|
let list = (options.force) ? state.list : [...(state._originalList || [])];
|
||||||
|
|
||||||
|
if (!options.force) {
|
||||||
|
Object.keys(targetFilters).forEach(fieldId => {
|
||||||
|
const val = targetFilters[fieldId];
|
||||||
|
if (val) {
|
||||||
|
const lowerVal = String(val).toLowerCase();
|
||||||
|
list = list.filter(item => String(item[fieldId] ?? '').toLowerCase().includes(lowerVal));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (targetSort && targetSort.fieldId && targetSort.direction) {
|
||||||
|
list.sort((a, b) => {
|
||||||
|
let va = a[targetSort.fieldId], vb = b[targetSort.fieldId];
|
||||||
|
if (va === vb) return 0;
|
||||||
|
if (va === null || va === undefined) return 1;
|
||||||
|
if (vb === null || vb === undefined) return -1;
|
||||||
|
const res = va > vb ? 1 : -1;
|
||||||
|
return targetSort.direction === 'asc' ? res : -res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__perfTrace = { evalCount: 0, evalTotal: 0 };
|
||||||
|
const frameStart = performance.now();
|
||||||
|
state._appliedHash = currentHash;
|
||||||
|
if (options.sort !== undefined) state.sortConfig = targetSort;
|
||||||
|
state.list = list;
|
||||||
|
state._internalUpdate = false;
|
||||||
|
|
||||||
|
const frameEnd = performance.now();
|
||||||
|
const totalTime = frameEnd - startTime;
|
||||||
|
console.log(`[DataTable Performance Profile] Sync Block: ${totalTime.toFixed(2)}ms (Eval: ${window.__perfTrace.evalCount})`);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(`[DataTable Performance Profile] E2E Paint: ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.__perfTrace = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
container.showColumnMenu = (field, e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const btn = e.currentTarget;
|
||||||
|
const rect = btn.getBoundingClientRect();
|
||||||
|
const rootRect = container.getBoundingClientRect();
|
||||||
|
const menu = container.querySelector('.dt-column-menu');
|
||||||
|
state.activeFieldId = field.id;
|
||||||
|
menu.style.display = 'block';
|
||||||
|
menu.style.left = (rect.right - rootRect.left - 180) + 'px';
|
||||||
|
menu.style.top = (rect.bottom - rootRect.top + 5) + 'px';
|
||||||
|
const onGlobalClick = (ev) => {
|
||||||
|
if (menu.contains(ev.target)) return;
|
||||||
|
if (!btn.contains(ev.target)) {
|
||||||
|
container.hideColumnMenu();
|
||||||
|
container.applySortFilter();
|
||||||
|
document.removeEventListener('mousedown', onGlobalClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', onGlobalClick);
|
||||||
|
RefreshState(menu);
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = menu.querySelector('input');
|
||||||
|
if (input) input.focus();
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
container.hideColumnMenu = () => {
|
||||||
|
const menu = container.querySelector('.dt-column-menu');
|
||||||
|
if (menu) menu.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
container.setSort = (direction) => {
|
||||||
|
container.applySortFilter({ sort: direction });
|
||||||
|
};
|
||||||
|
|
||||||
|
container.clearColumnSettings = () => {
|
||||||
|
const filters = { ...state.filterConfig };
|
||||||
|
delete filters[state.activeFieldId];
|
||||||
|
state.filterConfig = filters;
|
||||||
|
container.applySortFilter({ sort: null });
|
||||||
|
container.hideColumnMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Optimized Context Initialization (Using _refExt) ---
|
||||||
|
container._initRow = (rowNode) => {
|
||||||
|
if (!rowNode || !rowNode.children) return;
|
||||||
|
// This runs during framework's parsing of the cloned row.
|
||||||
|
// We set _refExt on each cell clone BEFORE _scanTree visits them.
|
||||||
|
const cells = rowNode.children;
|
||||||
|
for (let i = 0; i < cells.length; i++) {
|
||||||
|
const cell = cells[i];
|
||||||
|
const fIdx = parseInt(cell.dataset.fidx);
|
||||||
|
if (!isNaN(fIdx)) {
|
||||||
|
cell._refExt = { f: state.fields[fIdx], fIdx: fIdx };
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Optimized Event Delegation & DOM Interaction ---
|
|
||||||
container.onMainMouseDown = e => {
|
container.onMainMouseDown = e => {
|
||||||
const cell = e.target.closest('.dt-cell');
|
const cell = e.target.closest('.dt-cell');
|
||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
const row = cell.closest('.dt-row');
|
const row = cell.closest('.dt-row');
|
||||||
if (!row || row.classList.contains('dt-header-row')) return;
|
if (!row || row.classList.contains('dt-header-row')) return;
|
||||||
|
const fIdx = cell.dataset.fidx !== undefined ? parseInt(cell.dataset.fidx) : (cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell));
|
||||||
const fIdx = cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell);
|
|
||||||
const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
|
const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
|
||||||
const absoluteRow = rIdx + state._listStartIndex;
|
container.startSelect(rIdx + state._listStartIndex, fIdx, e);
|
||||||
container.startSelect(absoluteRow, fIdx, e);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
container.onMainMouseOver = e => {
|
container.onMainMouseOver = e => {
|
||||||
@ -82,11 +207,9 @@ Component.register('DataTable', container => {
|
|||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
const row = cell.closest('.dt-row');
|
const row = cell.closest('.dt-row');
|
||||||
if (!row || row.classList.contains('dt-header-row')) return;
|
if (!row || row.classList.contains('dt-header-row')) return;
|
||||||
|
const fIdx = cell.dataset.fidx !== undefined ? parseInt(cell.dataset.fidx) : (cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell));
|
||||||
const fIdx = cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell);
|
|
||||||
const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
|
const rIdx = row._ref?.rIdx ?? Array.from(container.querySelectorAll('.dt-body-row')).indexOf(row);
|
||||||
const absoluteRow = rIdx + state._listStartIndex;
|
container.updateSelect(rIdx + state._listStartIndex, fIdx);
|
||||||
container.updateSelect(absoluteRow, fIdx);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
container.onMainDblClick = e => {
|
container.onMainDblClick = e => {
|
||||||
@ -94,64 +217,73 @@ Component.register('DataTable', container => {
|
|||||||
if (!cell) return;
|
if (!cell) return;
|
||||||
const row = cell.closest('.dt-row');
|
const row = cell.closest('.dt-row');
|
||||||
if (!row || row.classList.contains('dt-header-row')) return;
|
if (!row || row.classList.contains('dt-header-row')) return;
|
||||||
|
|
||||||
const item = row._ref?.item;
|
const item = row._ref?.item;
|
||||||
const field = cell._ref?.f;
|
const fIdx = cell.dataset.fidx !== undefined ? parseInt(cell.dataset.fidx) : (cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell));
|
||||||
|
const field = cell._ref?.f ?? state.fields[fIdx];
|
||||||
if (item && field) container.editCell(item, field, cell);
|
if (item && field) container.editCell(item, field, cell);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state.__watch('fields', fields => {
|
||||||
|
if (!fields) return;
|
||||||
|
state._fieldsDirty = true;
|
||||||
|
const gridTemplate = fields.map(f => `var(--w-${f.id}, ${f.width || 150}px)`).join(' ');
|
||||||
|
const totalWidth = fields.reduce((sum, f) => sum + (f.width || 150), 0);
|
||||||
|
container.style.setProperty('--dt-grid-template', gridTemplate);
|
||||||
|
container.style.setProperty('--dt-row-width', totalWidth + 'px');
|
||||||
|
})
|
||||||
|
|
||||||
state.__watch('list', list => {
|
state.__watch('list', list => {
|
||||||
|
if (state._fieldsDirty) {
|
||||||
|
state._fieldsDirty = false;
|
||||||
|
const rowTemplate = container.querySelector('.dt-body template[index="rIdx"]');
|
||||||
|
if (rowTemplate) {
|
||||||
|
const fieldTemplate = rowTemplate.content.querySelector('template[as="f"]');
|
||||||
|
if (fieldTemplate) {
|
||||||
|
// 1. Capture the master copy of cell nodes once
|
||||||
|
if (!state._masterCellNodes) {
|
||||||
|
state._masterCellNodes = Array.from(fieldTemplate.content.childNodes).map(n => n.cloneNode(true));
|
||||||
|
}
|
||||||
|
// 2. Transform the field template into a static flattened block
|
||||||
|
fieldTemplate.removeAttribute('$each');
|
||||||
|
fieldTemplate.removeAttribute('as');
|
||||||
|
fieldTemplate.removeAttribute('index');
|
||||||
|
fieldTemplate.setAttribute('$if', 'true'); // High-speed safe released block
|
||||||
|
fieldTemplate.content.textContent = ''; // Idiomatic way to clear document fragment
|
||||||
|
|
||||||
|
// 3. Inject flattened columns with position markers
|
||||||
|
state.fields.forEach((f, fIdx) => {
|
||||||
|
state._masterCellNodes.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 || [])];
|
||||||
scroll.init();
|
scroll.init();
|
||||||
scroll.reset(list);
|
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Editing Logic (Manual Control, Permanent DOM) ---
|
|
||||||
let currentEditingNode = null;
|
let currentEditingNode = null;
|
||||||
|
|
||||||
container.editCell = (row, field, cellNode) => {
|
container.editCell = (row, field, cellNode) => {
|
||||||
const overlay = container.querySelector('.dt-editor-overlay');
|
const overlay = container.querySelector('.dt-editor-overlay');
|
||||||
const editor = overlay.querySelector('AutoForm');
|
const editor = overlay.querySelector('AutoForm');
|
||||||
const rect = cellNode.getBoundingClientRect();
|
const rect = cellNode.getBoundingClientRect();
|
||||||
const rootRect = container.getBoundingClientRect();
|
const rootRect = container.getBoundingClientRect();
|
||||||
|
|
||||||
currentEditingNode = cellNode;
|
currentEditingNode = cellNode;
|
||||||
|
|
||||||
// 1. Precise Positioning with expansion and Z-index
|
|
||||||
let minW = rect.width;
|
let minW = rect.width;
|
||||||
if (field.type === 'textarea' || field.type === 'TagsInput') minW = Math.max(rect.width, 300);
|
if (field.type === 'textarea' || field.type === 'TagsInput') minW = Math.max(rect.width, 300);
|
||||||
else if (field.type === 'radio') minW = Math.max(rect.width, 240);
|
else if (field.type === 'radio') minW = Math.max(rect.width, 240);
|
||||||
|
|
||||||
overlay.style.display = 'flex';
|
overlay.style.display = 'flex';
|
||||||
overlay.style.left = (rect.left - rootRect.left) + 'px';
|
overlay.style.left = (rect.left - rootRect.left) + 'px';
|
||||||
overlay.style.top = (rect.top - rootRect.top) + 'px';
|
overlay.style.top = (rect.top - rootRect.top) + 'px';
|
||||||
overlay.style.width = minW + 'px';
|
overlay.style.width = minW + 'px';
|
||||||
|
overlay.style.height = (field.type === 'textarea' || field.type === 'TagsInput' ? 'auto' : rect.height + 'px');
|
||||||
// Reset styles to prevent crosstalk from previous editor types
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Direct Driver: Switch context
|
|
||||||
// Use global State for both schema and data to ensure atomic updates
|
|
||||||
State.editingSchema = [{ ...field, name: field.id, label: '' }];
|
State.editingSchema = [{ ...field, name: field.id, label: '' }];
|
||||||
State.editingData = row;
|
State.editingData = row;
|
||||||
RefreshState(overlay);
|
RefreshState(overlay);
|
||||||
|
|
||||||
// 3. Focus
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const input = editor.querySelector('.form-control, .form-select, .form-check-input, input');
|
const input = editor.querySelector('.form-control, .form-select, .form-check-input, input');
|
||||||
if (input) input.focus();
|
if (input) input.focus();
|
||||||
@ -161,51 +293,27 @@ Component.register('DataTable', container => {
|
|||||||
container.hideEditor = (save = true) => {
|
container.hideEditor = (save = true) => {
|
||||||
if (!_editorOverlay) _editorOverlay = container.querySelector('.dt-editor-overlay');
|
if (!_editorOverlay) _editorOverlay = container.querySelector('.dt-editor-overlay');
|
||||||
if (!_editorOverlay || _editorOverlay.style.display === 'none') return;
|
if (!_editorOverlay || _editorOverlay.style.display === 'none') return;
|
||||||
|
|
||||||
_editorOverlay.style.display = 'none';
|
_editorOverlay.style.display = 'none';
|
||||||
if (save && currentEditingNode) {
|
if (save && currentEditingNode) RefreshState(currentEditingNode);
|
||||||
RefreshState(currentEditingNode);
|
State.editingSchema = State.editingData = currentEditingNode = null;
|
||||||
}
|
|
||||||
State.editingSchema = null;
|
|
||||||
State.editingData = null;
|
|
||||||
currentEditingNode = null;
|
|
||||||
container.focus();
|
container.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Shared Logic ---
|
|
||||||
container.startSelect = selection.startSelect;
|
container.startSelect = selection.startSelect;
|
||||||
container.updateSelect = selection.updateSelect;
|
container.updateSelect = selection.updateSelect;
|
||||||
container.deleteSelected = selection.deleteSelected;
|
container.deleteSelected = selection.deleteSelected;
|
||||||
|
|
||||||
container.addEventListener('keydown', e => {
|
|
||||||
if (e.ctrlKey || e.metaKey) {
|
|
||||||
const k = e.key.toLowerCase();
|
|
||||||
if (k === 'c') {
|
|
||||||
e.preventDefault();
|
|
||||||
selection.copy();
|
|
||||||
}
|
|
||||||
if (k === 'v') {
|
|
||||||
e.preventDefault();
|
|
||||||
selection.paste();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape') container.hideEditor(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
const onGlobalMouseDown = e => {
|
const onGlobalMouseDown = e => {
|
||||||
const overlay = container.querySelector('.dt-editor-overlay');
|
const overlay = container.querySelector('.dt-editor-overlay');
|
||||||
if (overlay && overlay.style.display !== 'none' && !overlay.contains(e.target)) {
|
if (overlay && overlay.style.display !== 'none' && !overlay.contains(e.target)) container.hideEditor(true);
|
||||||
container.hideEditor(true);
|
|
||||||
}
|
|
||||||
if (!container.contains(e.target) && !overlay?.contains(e.target)) selection.clearAllActive();
|
if (!container.contains(e.target) && !overlay?.contains(e.target)) selection.clearAllActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('mouseup', selection.endSelect);
|
window.addEventListener('mouseup', selection.endSelect);
|
||||||
document.addEventListener('mousedown', onGlobalMouseDown);
|
document.addEventListener('mousedown', onGlobalMouseDown);
|
||||||
|
|
||||||
container._onUnload = () => {
|
container._onUnload = () => {
|
||||||
document.removeEventListener('mousedown', onGlobalMouseDown)
|
document.removeEventListener('mousedown', onGlobalMouseDown);
|
||||||
window.removeEventListener('mouseup', selection.endSelect)
|
window.removeEventListener('mouseup', selection.endSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, Util.makeDom(/*html*/`
|
}, Util.makeDom(/*html*/`
|
||||||
@ -217,35 +325,70 @@ Component.register('DataTable', container => {
|
|||||||
style="overflow-anchor:none; min-height: 0">
|
style="overflow-anchor:none; min-height: 0">
|
||||||
<div class="dt-header border-bottom bg-light sticky-top" style="z-index:20">
|
<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 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">
|
<template $each="this.state?.fields || []">
|
||||||
<span $text="item.name" class="text-truncate"></span>
|
<div $data-id="item.id" class="dt-cell border-end px-2 d-flex align-items-center justify-content-between header-cell">
|
||||||
</div>
|
<div class="d-flex align-items-center overflow-hidden">
|
||||||
|
<i $if="this.state?.sortConfig?.fieldId === item.id"
|
||||||
|
$class="'bi bi-sort-' + (this.state?.sortConfig?.direction === 'asc' ? 'down' : 'up-alt') + ' me-1 text-primary'"></i>
|
||||||
|
<i $if="this.state?.filterConfig?.[item.id]" class="bi bi-filter me-1 text-primary"></i>
|
||||||
|
<span $text="item.name" class="text-truncate"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-xs btn-link text-muted p-0 border-0 header-menu-btn" $onclick="this.showColumnMenu(item, event)">
|
||||||
|
<i class="bi bi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dt-body" style="position:relative">
|
<div class="dt-body" style="position:relative">
|
||||||
<div class="dt-spacer-prev flex-shrink-0" style="display:none"></div>
|
<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">
|
<template $each="this.state?._renderedList || []" key="id" index="rIdx">
|
||||||
<div $each="this.state?.fields || []" as="f" index="fIdx" class="dt-cell border-end px-2 d-flex align-items-center">
|
<div class="dt-row dt-body-row border-bottom bg-white" $.="this._initRow(thisNode)">
|
||||||
<span $text="this.format(item[f.id], f)" class="text-truncate"></span>
|
<template $each="this.state?.fields || []" as="f" index="fIdx">
|
||||||
|
<div class="dt-cell border-end px-2 d-flex align-items-center">
|
||||||
|
<span $text="!f.formatter ? item[f.id] : this.format(item[f.id], f)" class="text-truncate"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="dt-spacer-post flex-shrink-0" style="display:none"></div>
|
<div class="dt-spacer-post flex-shrink-0" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Shared Editor Overlay -->
|
<div class="dt-column-menu border bg-body shadow-sm rounded" style="display:none; position:absolute; z-index:2000; min-width:180px" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
||||||
<div class="dt-editor-overlay" style="display: none" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
<div class="list-group list-group-flush">
|
||||||
|
<button class="list-group-item list-group-item-action py-2 d-flex align-items-center" $onclick="this.setSort('asc')">
|
||||||
|
<i class="bi bi-sort-alpha-down me-2"></i> Sort Ascending
|
||||||
|
</button>
|
||||||
|
<button class="list-group-item list-group-item-action py-2 d-flex align-items-center" $onclick="this.setSort('desc')">
|
||||||
|
<i class="bi bi-sort-alpha-up-alt me-2"></i> Sort Descending
|
||||||
|
</button>
|
||||||
|
<div class="p-2 border-top bg-light-subtle">
|
||||||
|
<div class="d-flex align-items-center mb-1 text-muted fw-bold" style="font-size: 10px">
|
||||||
|
<i class="bi bi-filter me-1"></i> LOCAL FILTER
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control form-control-sm" placeholder="Search..."
|
||||||
|
$bind="this.state.filterConfig[this.state.activeFieldId]"
|
||||||
|
$onkeydown="if(event.key === 'Enter') this.applySortFilter()">
|
||||||
|
</div>
|
||||||
|
<button class="list-group-item list-group-item-action py-2 text-danger d-flex align-items-center border-top" $onclick="this.clearColumnSettings()">
|
||||||
|
<i class="bi bi-x-circle me-2"></i> Clear Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dt-editor-overlay dt-editor-container" style="display: none" $onclick="event.stopPropagation()" $onmousedown="event.stopPropagation()">
|
||||||
<AutoForm inline class="h-100 w-100" $onsubmit="thisNode.closest('DataTable').hideEditor(true)"/>
|
<AutoForm inline class="h-100 w-100" $onsubmit="thisNode.closest('DataTable').hideEditor(true)"/>
|
||||||
</div>
|
</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="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">
|
<div class="d-flex align-items-center gap-2">
|
||||||
Total: <span $text="this.state?.list?.length || 0"></span> |
|
<span $text="(this.state?.selectedRowCount || 0) + ' / ' + (this.state?.list?.length || 0)"></span>
|
||||||
Selected Rows: <span $text="this.state?.selectedRowCount || 0"></span>
|
<button class="btn btn-xs btn-link text-danger p-0 border-0" title="Delete Selected"
|
||||||
|
$if="this.state?.selectedRowCount" $onclick="this.deleteSelected()">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`), Util.makeDom(/*html*/`
|
`), Util.makeDom(/*html*/`
|
||||||
@ -255,57 +398,27 @@ Component.register('DataTable', container => {
|
|||||||
.dt-row {
|
.dt-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--dt-grid-template);
|
grid-template-columns: var(--dt-grid-template);
|
||||||
width: max-content;
|
width: var(--dt-row-width, max-content);
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
contain: paint layout;
|
||||||
}
|
}
|
||||||
.dt-header-row {
|
.dt-header-row { background-color: var(--bs-tertiary-bg); }
|
||||||
background-color: var(--bs-tertiary-bg);
|
.dt-cell { background: inherit; white-space: nowrap; flex-shrink: 0; contain: content; }
|
||||||
}
|
.dt-body-row:hover { background-color: var(--bs-secondary-bg) !important; }
|
||||||
.dt-cell {
|
.dt-cell-selected { background-color: var(--bs-primary-bg-subtle) !important; }
|
||||||
background: inherit;
|
.header-cell .header-menu-btn { opacity: 0; transition: opacity 0.2s; }
|
||||||
white-space: nowrap;
|
.header-cell:hover .header-menu-btn { opacity: 1; }
|
||||||
flex-shrink: 0;
|
.dt-column-menu { box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; }
|
||||||
}
|
.dt-column-menu .list-group-item { border: none; cursor: pointer; }
|
||||||
.dt-body-row:hover {
|
.dt-column-menu .list-group-item:hover { background-color: var(--bs-light); }
|
||||||
background-color: var(--bs-secondary-bg) !important;
|
|
||||||
}
|
|
||||||
.dt-cell-selected {
|
|
||||||
background-color: var(--bs-primary-bg-subtle) !important;
|
|
||||||
}
|
|
||||||
.dt-editor-overlay {
|
.dt-editor-overlay {
|
||||||
position: absolute;
|
position: absolute; z-index: 1000; background: var(--bs-body-bg);
|
||||||
z-index: 1000;
|
box-shadow: 0 4px 16px rgba(0,0,0,0.25); border: 1px solid var(--bs-primary);
|
||||||
background: var(--bs-body-bg);
|
align-items: center; overflow: visible;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
.dt-editor-overlay .form-control { border: none !important; border-radius: 0 !important; height: 100% !important; padding: 0 8px !important; }
|
||||||
|
.btn-xs { padding: 1px 5px; line-height: 1.5; }
|
||||||
</style>
|
</style>
|
||||||
`))
|
`))
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,13 @@ export const createScrollManager = (container, state, onRenderedListChange) => {
|
|||||||
const vs = VirtualScroll({ itemHeight: 40 });
|
const vs = VirtualScroll({ itemHeight: 40 });
|
||||||
let scrollEl = null;
|
let scrollEl = null;
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = (isLayoutChange = false) => {
|
||||||
if (!scrollEl) return;
|
if (!scrollEl) return;
|
||||||
const res = vs.calc(scrollEl, state.list);
|
const res = vs.calc(scrollEl, state.list);
|
||||||
if (res) {
|
if (res) {
|
||||||
// Dirty check: Abort if layout boundaries haven't actually changed
|
// Dirty check: Abort if layout boundaries haven't actually changed
|
||||||
if (state.prevHeight === res.prevHeight &&
|
if (!isLayoutChange &&
|
||||||
|
state.prevHeight === res.prevHeight &&
|
||||||
state.postHeight === res.postHeight &&
|
state.postHeight === res.postHeight &&
|
||||||
state._listStartIndex === res.listStartIndex &&
|
state._listStartIndex === res.listStartIndex &&
|
||||||
state._renderedList.length === res.renderedList.length) {
|
state._renderedList.length === res.renderedList.length) {
|
||||||
@ -22,7 +23,7 @@ export const createScrollManager = (container, state, onRenderedListChange) => {
|
|||||||
_listStartIndex: res.listStartIndex,
|
_listStartIndex: res.listStartIndex,
|
||||||
_renderedList: res.renderedList
|
_renderedList: res.renderedList
|
||||||
});
|
});
|
||||||
onRenderedListChange?.(res.renderedList.length);
|
onRenderedListChange?.(res.renderedList.length, isLayoutChange);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,12 +33,14 @@ export const createScrollManager = (container, state, onRenderedListChange) => {
|
|||||||
},
|
},
|
||||||
reset: (list) => {
|
reset: (list) => {
|
||||||
state._listStartIndex = 0;
|
state._listStartIndex = 0;
|
||||||
state._renderedList = vs.reset(list, scrollEl || container) || [];
|
// 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) {
|
if (state.list === list) {
|
||||||
vs.init(list, refresh);
|
vs.init(list, () => refresh(true));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh,
|
refresh,
|
||||||
onScroll: refresh
|
onScroll: () => refresh(false)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,9 @@ export const createSelectionManager = (container, state) => {
|
|||||||
let lastHadSelection = false;
|
let lastHadSelection = false;
|
||||||
|
|
||||||
const applySelectionUI = () => {
|
const applySelectionUI = () => {
|
||||||
|
/* DEBUG: Performance Isolation Toggle */
|
||||||
|
if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return;
|
||||||
|
|
||||||
let boundMinRow = Infinity, boundMaxRow = -Infinity;
|
let boundMinRow = Infinity, boundMaxRow = -Infinity;
|
||||||
if (activeBounds) {
|
if (activeBounds) {
|
||||||
boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
|
boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
|
||||||
@ -22,41 +25,35 @@ export const createSelectionManager = (container, state) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hasSelection = boundMinRow !== Infinity;
|
const hasSelection = boundMinRow !== Infinity;
|
||||||
|
|
||||||
// Fast path: If no current selection AND last frame also had no selection, skip EVERYTHING.
|
|
||||||
if (!hasSelection && !lastHadSelection) return;
|
if (!hasSelection && !lastHadSelection) return;
|
||||||
lastHadSelection = hasSelection;
|
lastHadSelection = hasSelection;
|
||||||
|
|
||||||
const body = container.querySelector('.dt-body');
|
const body = container.querySelector('.dt-body');
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
const rowNodes = body.children;
|
|
||||||
|
|
||||||
// Fast path for scroll performance: Only iterate if selection overlaps or we need to clear
|
// Fast path: Use children instead of querySelectorAll
|
||||||
for (let r = 0; r < rowNodes.length; r++) {
|
const nodes = body.children;
|
||||||
const rowNode = rowNodes[r];
|
for (let r = 0; r < nodes.length; r++) {
|
||||||
|
const rowNode = nodes[r];
|
||||||
if (!rowNode.classList.contains('dt-body-row')) continue;
|
if (!rowNode.classList.contains('dt-body-row')) continue;
|
||||||
|
|
||||||
const absoluteRow = (rowNode._ref?.rIdx ?? -1) + state._listStartIndex;
|
const absoluteRow = (rowNode._ref?.rIdx ?? -1) + state._listStartIndex;
|
||||||
const cellNodes = rowNode.children;
|
|
||||||
|
|
||||||
// Fast path: Row completely outside selection
|
|
||||||
if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
|
if (!hasSelection || absoluteRow < boundMinRow || absoluteRow > boundMaxRow) {
|
||||||
|
// Clear all cells in row without checking individually if possible
|
||||||
|
const cellNodes = rowNode.children;
|
||||||
for (let i = 0; i < cellNodes.length; i++) {
|
for (let i = 0; i < cellNodes.length; i++) {
|
||||||
if (cellNodes[i].classList.contains('dt-cell-selected')) {
|
cellNodes[i].classList.remove('dt-cell-selected');
|
||||||
cellNodes[i].classList.remove('dt-cell-selected');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cellNodes = rowNode.children;
|
||||||
for (let i = 0; i < cellNodes.length; i++) {
|
for (let i = 0; i < cellNodes.length; i++) {
|
||||||
const isSelected = isCellSelected(absoluteRow, i);
|
const isSelected = isCellSelected(absoluteRow, i);
|
||||||
const hasClass = cellNodes[i].classList.contains('dt-cell-selected');
|
const cell = cellNodes[i];
|
||||||
if (isSelected && !hasClass) {
|
if (isSelected) cell.classList.add('dt-cell-selected');
|
||||||
cellNodes[i].classList.add('dt-cell-selected');
|
else cell.classList.remove('dt-cell-selected');
|
||||||
} else if (!isSelected && hasClass) {
|
|
||||||
cellNodes[i].classList.remove('dt-cell-selected');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DataTable Demo</title>
|
<title>DataTable Demo</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body, html { height: 100%; margin: 0; padding: 0; overflow: hidden; }
|
body, html { height: 100%; margin: 0; padding: 0; overflow: hidden; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user