diff --git a/dist/datatable.js b/dist/datatable.js
index 7f9d547..b81c365 100644
--- a/dist/datatable.js
+++ b/dist/datatable.js
@@ -50,11 +50,11 @@ const createPerfMonitor = () => {
const createScrollManager = (container, state, onRenderedListChange) => {
const vs = VirtualScroll({ itemHeight: 40 });
let scrollEl = null;
- const refresh = () => {
+ const refresh = (isLayoutChange = false) => {
if (!scrollEl) return;
const res = vs.calc(scrollEl, state.list);
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;
}
Object.assign(state, {
@@ -63,7 +63,7 @@ const createScrollManager = (container, state, onRenderedListChange) => {
_listStartIndex: res.listStartIndex,
_renderedList: res.renderedList
});
- onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length);
+ onRenderedListChange == null ? void 0 : onRenderedListChange(res.renderedList.length, isLayoutChange);
}
};
return {
@@ -72,13 +72,13 @@ const createScrollManager = (container, state, onRenderedListChange) => {
},
reset: (list) => {
state._listStartIndex = 0;
- state._renderedList = vs.reset(list, scrollEl || container) || [];
+ vs.reset(list, scrollEl || container);
if (state.list === list) {
- vs.init(list, refresh);
+ vs.init(list, () => refresh(true));
}
},
refresh,
- onScroll: refresh
+ onScroll: () => refresh(false)
};
};
const createSelectionManager = (container, state) => {
@@ -92,6 +92,7 @@ const createSelectionManager = (container, state) => {
let lastHadSelection = false;
const applySelectionUI = () => {
var _a;
+ if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return;
let boundMinRow = Infinity, boundMaxRow = -Infinity;
if (activeBounds) {
boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
@@ -106,28 +107,24 @@ const createSelectionManager = (container, state) => {
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];
+ const nodes = body.children;
+ for (let r = 0; r < nodes.length; r++) {
+ const rowNode = nodes[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");
- }
+ const cellNodes2 = rowNode.children;
+ for (let i = 0; i < cellNodes2.length; i++) {
+ cellNodes2[i].classList.remove("dt-cell-selected");
}
continue;
}
+ const cellNodes = rowNode.children;
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");
- }
+ const cell = cellNodes[i];
+ if (isSelected) cell.classList.add("dt-cell-selected");
+ else cell.classList.remove("dt-cell-selected");
}
}
};
@@ -312,13 +309,20 @@ Component.register("DataTable", (container) => {
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();
state.perf = perf.stats;
const selection = createSelectionManager(container, state);
- const scroll = createScrollManager(container, state, (renderedCount) => {
- container.hideEditor();
+ const scroll = createScrollManager(container, state, (renderedCount, isLayoutChange) => {
selection.applySelectionUI();
});
let _prevSpacer, _postSpacer, _editorOverlay;
@@ -346,9 +350,119 @@ Component.register("DataTable", (container) => {
}
perf.endFrame(frameStart, state._renderedList.length);
};
+ let _scrollRaf = null;
container.onScroll = () => {
perf.onScroll();
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) => {
var _a, _b;
@@ -356,10 +470,9 @@ Component.register("DataTable", (container) => {
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 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 absoluteRow = rIdx + state._listStartIndex;
- container.startSelect(absoluteRow, fIdx, e);
+ container.startSelect(rIdx + state._listStartIndex, fIdx, e);
};
container.onMainMouseOver = (e) => {
var _a, _b;
@@ -368,29 +481,57 @@ Component.register("DataTable", (container) => {
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 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 absoluteRow = rIdx + state._listStartIndex;
- container.updateSelect(absoluteRow, fIdx);
+ container.updateSelect(rIdx + state._listStartIndex, fIdx);
};
container.onMainDblClick = (e) => {
- var _a, _b;
+ var _a, _b, _c;
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;
+ 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);
};
- state.__watch("list", (list) => {
- scroll.init();
- scroll.reset(list);
- });
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) => {
+ 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;
container.editCell = (row, field, cellNode) => {
@@ -406,16 +547,7 @@ Component.register("DataTable", (container) => {
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";
- }
+ overlay.style.height = field.type === "textarea" || field.type === "TagsInput" ? "auto" : rect.height + "px";
State.editingSchema = [{ ...field, name: field.id, label: "" }];
State.editingData = row;
RefreshState(overlay);
@@ -428,36 +560,16 @@ Component.register("DataTable", (container) => {
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;
+ if (save && currentEditingNode) RefreshState(currentEditingNode);
+ State.editingSchema = State.editingData = currentEditingNode = null;
container.focus();
};
container.startSelect = selection.startSelect;
container.updateSelect = selection.updateSelect;
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 overlay = container.querySelector(".dt-editor-overlay");
- if (overlay && overlay.style.display !== "none" && !overlay.contains(e.target)) {
- container.hideEditor(true);
- }
+ 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);
@@ -477,35 +589,70 @@ Component.register("DataTable", (container) => {
style="overflow-anchor:none; min-height: 0">
-
-
+
+
+
+
+
+
+
+
+
+
`
@@ -518,57 +665,27 @@ Component.register("DataTable", (container) => {
.dt-row {
display: grid;
grid-template-columns: var(--dt-grid-template);
- width: max-content;
+ width: var(--dt-row-width, max-content);
min-width: 100%;
height: 40px;
+ contain: paint layout;
}
- .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-header-row { 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-selected { background-color: var(--bs-primary-bg-subtle) !important; }
+ .header-cell .header-menu-btn { opacity: 0; transition: opacity 0.2s; }
+ .header-cell:hover .header-menu-btn { opacity: 1; }
+ .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-column-menu .list-group-item:hover { background-color: var(--bs-light); }
.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;
+ 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;
}
+ .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; }
`
));
diff --git a/dist/datatable.min.js b/dist/datatable.min.js
index 6695b60..28db246 100644
--- a/dist/datatable.min.js
+++ b/dist/datatable.min.js
@@ -1 +1 @@
-import{Component as t,NewState as e,RefreshState as n,Util as o}from"@web/state";import{VirtualScroll as r,State as l}from"@web/base";t.register("DataTable",t=>{t.state||(t.state=e({}));const o=t.state;Object.assign(o,{list:[],fields:[],_renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selectedRowCount:0});const i=(()=>{let t=!!window.__DT_PERF_MODE__;const e={refreshTime:0,refreshCount:0,scrollCount:0,totalNodes:0};return t&&!window.__statePerformanceTelemetry&&(window.__statePerformanceTelemetry={scanCount:0,reuseCount:0,moveCount:0}),{get stats(){return e},enable:()=>{t=!0},disable:()=>{t=!1},onScroll:()=>{t&&e.scrollCount++},startFrame:()=>{var e,n,o;return t?{start:performance.now(),scan:(null==(e=window.__statePerformanceTelemetry)?void 0:e.scanCount)||0,move:(null==(n=window.__statePerformanceTelemetry)?void 0:n.moveCount)||0,reuse:(null==(o=window.__statePerformanceTelemetry)?void 0:o.reuseCount)||0}:null},endFrame:(n,o)=>{if(!t||!n)return;e.refreshCount++,e.totalNodes+=o;const r=performance.now()-n.start;e.refreshTime+=r;const l=window.__statePerformanceTelemetry;if(l){const t=l.scanCount-n.scan,e=l.moveCount-n.move,i=l.reuseCount-n.reuse;(t>0||r>2)&&console.log(`[DataTable Frame] Time: ${r.toFixed(2)}ms, Scans: ${t}, Moves: ${e}, Reuses: ${i}, Rows: ${o}`)}}}})();o.perf=i.stats;const s=((t,e)=>{let n=null,o=null,r=[];const l=(t,e)=>!!(n&&t>=n.minRow&&t<=n.maxRow&&e>=n.minCol&&e<=n.maxCol)||r.some(n=>t>=n.minRow&&t<=n.maxRow&&e>=n.minCol&&e<=n.maxCol);let i=!1;const s=()=>{var o;let s=1/0,a=-1/0;n&&(s=Math.min(s,n.minRow),a=Math.max(a,n.maxRow)),r.forEach(t=>{s=Math.min(s,t.minRow),a=Math.max(a,t.maxRow)});const d=s!==1/0;if(!d&&!i)return;i=d;const c=t.querySelector(".dt-body");if(!c)return;const m=c.children;for(let t=0;t
a)for(let t=0;t{let t=0;n&&(t+=n.maxRow-n.minRow+1),r.forEach(e=>t+=e.maxRow-e.minRow+1),e.selectedRowCount=t},d=(t=!1)=>{t||(n=null,o=null,r=[],s(),a())},c=()=>{if(!n)return null;let t=n.minRow,e=n.maxRow,o=n.minCol,l=n.maxCol;return r.forEach(n=>{t=Math.min(t,n.minRow),e=Math.max(e,n.maxRow),o=Math.min(o,n.minCol),l=Math.max(l,n.maxCol)}),{minRow:t,maxRow:e,minCol:o,maxCol:l}};return{applySelectionUI:s,clearAllActive:d,startSelect:(i,c,m)=>{const h=l(i,c),u=n&&(n.minRow!==n.maxRow||n.minCol!==n.maxCol)||r.length>0;m.shiftKey&&o?n={minRow:Math.min(o.row,i),maxRow:Math.max(o.row,i),minCol:Math.min(o.col,c),maxCol:Math.max(o.col,c)}:(!h||m.ctrlKey||m.metaKey?(m.ctrlKey||m.metaKey?n&&!h&&r.push(n):d(),o={row:i,col:c},n={minRow:i,maxRow:i,minCol:c,maxCol:c}):u||(t._potentialCancel={row:i,col:c}),e.isSelecting=!0),s(),a(),t.focus()},updateSelect:(r,l)=>{e.isSelecting&&o&&(n={minRow:Math.min(o.row,r),maxRow:Math.max(o.row,r),minCol:Math.min(o.col,l),maxCol:Math.max(o.col,l)},t._potentialCancel=null,s(),a())},endSelect:()=>{if(t._potentialCancel){const{row:e,col:n}=t._potentialCancel;l(e,n)&&d(),t._potentialCancel=null}e.isSelecting=!1},deleteSelected:()=>{const t=c();if(!t)return;const n=t.minRow,o=t.maxRow,r=e.list.filter((t,e)=>!(e>=n&&e<=o));e.list=r,d()},getSelectionBounds:c,copy:async()=>{const t=c();if(!t)return;const n=e.list.slice(t.minRow,t.maxRow+1).map(n=>e.fields.slice(t.minCol,t.maxCol+1).map(t=>{let e=String(n[t.id]??"");return(e.includes("\t")||e.includes("\n")||e.includes('"'))&&(e='"'+e.replace(/"/g,'""')+'"'),e}).join("\t")).join("\n");await navigator.clipboard.writeText(n)},paste:async()=>{try{const n=await navigator.clipboard.readText();if(!n)return;const o=c();if(!o)return;const r=n.split(/\r?\n/).filter(t=>t.length>0).map(t=>{const e=[];let n="",o=!1;for(let r=0;r{var e;return null==(e=t.classList)?void 0:e.contains("dt-body-row")}):[];r.forEach((n,o)=>{const r=l+o;if(r>s||r>=e.list.length)return;const d=e.list[r];let c=!1;if(n.forEach((t,n)=>{const o=i+n;if(o>a||o>=e.fields.length)return;const r=e.fields[o];d[r.id]=t,c=!0}),c&&t.refreshNode){const n=m.find(t=>{var n;return((null==(n=t._ref)?void 0:n.rIdx)??-1)+e._listStartIndex===r});n&&t.refreshNode(n)}})}catch(t){console.error("Paste Error:",t)}}}})(t,o),a=((t,e,n)=>{const o=r({itemHeight:40});let l=null;const i=()=>{if(!l)return;const t=o.calc(l,e.list);if(t){if(e.prevHeight===t.prevHeight&&e.postHeight===t.postHeight&&e._listStartIndex===t.listStartIndex&&e._renderedList.length===t.renderedList.length)return;Object.assign(e,{prevHeight:t.prevHeight,postHeight:t.postHeight,_listStartIndex:t.listStartIndex,_renderedList:t.renderedList}),null==n||n(t.renderedList.length)}};return{init:()=>{l=t.querySelector(".dt-main")},reset:n=>{e._listStartIndex=0,e._renderedList=o.reset(n,l||t)||[],e.list===n&&o.init(n,i)},refresh:i,onScroll:i}})(t,o,e=>{t.hideEditor(),s.applySelectionUI()});let d,c,m;t.format=(t,e)=>e.formatter?e.formatter(t,e):"string"==typeof t?t:null==t?"":"object"==typeof t?JSON.stringify(t):String(t),t.refreshNode=t=>n(t),t.refresh=()=>{const e=i.startFrame();if(a.refresh(),d||(d=t.querySelector(".dt-spacer-prev")),d){const t=o.prevHeight||0;d.style.height=t+"px",d.style.display=t>0?"block":"none"}if(c||(c=t.querySelector(".dt-spacer-post")),c){const t=o.postHeight||0;c.style.height=t+"px",c.style.display=t>0?"block":"none"}i.endFrame(e,o._renderedList.length)},t.onScroll=()=>{i.onScroll(),t.refresh()},t.onMainMouseDown=e=>{var n,r;const l=e.target.closest(".dt-cell");if(!l)return;const i=l.closest(".dt-row");if(!i||i.classList.contains("dt-header-row"))return;const s=(null==(n=l._ref)?void 0:n.fIdx)??Array.from(i.children).indexOf(l),a=((null==(r=i._ref)?void 0:r.rIdx)??Array.from(t.querySelectorAll(".dt-body-row")).indexOf(i))+o._listStartIndex;t.startSelect(a,s,e)},t.onMainMouseOver=e=>{var n,r;if(!o.isSelecting)return;const l=e.target.closest(".dt-cell");if(!l)return;const i=l.closest(".dt-row");if(!i||i.classList.contains("dt-header-row"))return;const s=(null==(n=l._ref)?void 0:n.fIdx)??Array.from(i.children).indexOf(l),a=((null==(r=i._ref)?void 0:r.rIdx)??Array.from(t.querySelectorAll(".dt-body-row")).indexOf(i))+o._listStartIndex;t.updateSelect(a,s)},t.onMainDblClick=e=>{var n,o;const r=e.target.closest(".dt-cell");if(!r)return;const l=r.closest(".dt-row");if(!l||l.classList.contains("dt-header-row"))return;const i=null==(n=l._ref)?void 0:n.item,s=null==(o=r._ref)?void 0:o.f;i&&s&&t.editCell(i,s,r)},o.__watch("list",t=>{a.init(),a.reset(t)}),o.__watch("fields",e=>{if(!e)return;const n=e.map(t=>`var(--w-${t.id}, ${t.width||150}px)`).join(" ");t.style.setProperty("--dt-grid-template",n)});let h=null;t.editCell=(e,o,r)=>{const i=t.querySelector(".dt-editor-overlay"),s=i.querySelector("AutoForm"),a=r.getBoundingClientRect(),d=t.getBoundingClientRect();h=r;let c=a.width;"textarea"===o.type||"TagsInput"===o.type?c=Math.max(a.width,300):"radio"===o.type&&(c=Math.max(a.width,240)),i.style.display="flex",i.style.left=a.left-d.left+"px",i.style.top=a.top-d.top+"px",i.style.width=c+"px",i.style.height="",i.style.minHeight="",i.style.alignItems="center","textarea"===o.type||"TagsInput"===o.type?(i.style.minHeight=("textarea"===o.type?150:a.height)+"px",i.style.height="auto",i.style.alignItems="flex-start"):i.style.height=a.height+"px",l.editingSchema=[{...o,name:o.id,label:""}],l.editingData=e,n(i),setTimeout(()=>{const t=s.querySelector(".form-control, .form-select, .form-check-input, input");t&&t.focus()},30)},t.hideEditor=(e=!0)=>{m||(m=t.querySelector(".dt-editor-overlay")),m&&"none"!==m.style.display&&(m.style.display="none",e&&h&&n(h),l.editingSchema=null,l.editingData=null,h=null,t.focus())},t.startSelect=s.startSelect,t.updateSelect=s.updateSelect,t.deleteSelected=s.deleteSelected,t.addEventListener("keydown",e=>{if(e.ctrlKey||e.metaKey){const t=e.key.toLowerCase();"c"===t&&(e.preventDefault(),s.copy()),"v"===t&&(e.preventDefault(),s.paste())}"Escape"===e.key&&t.hideEditor(!1)});const u=e=>{const n=t.querySelector(".dt-editor-overlay");n&&"none"!==n.style.display&&!n.contains(e.target)&&t.hideEditor(!0),t.contains(e.target)||(null==n?void 0:n.contains(e.target))||s.clearAllActive()};window.addEventListener("mouseup",s.endSelect),document.addEventListener("mousedown",u),t._onUnload=()=>{document.removeEventListener("mousedown",u),window.removeEventListener("mouseup",s.endSelect)}},o.makeDom('\n\n\t
\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n\n\t\x3c!-- Shared Editor Overlay --\x3e\n\t
\n\n\t\n
\n'),o.makeDom("\n\n")),"undefined"!=typeof document&&n(document.documentElement);
+import{Component as t,NewState as e,RefreshState as n,Util as o}from"@web/state";import{VirtualScroll as i,State as r}from"@web/base";t.register("DataTable",t=>{t.state||(t.state=e({}));const o=t.state;Object.assign(o,{list:[],fields:[],_renderedList:[],prevHeight:0,postHeight:0,_listStartIndex:0,selectedRowCount:0,_originalList:[],sortConfig:{fieldId:null,direction:null},filterConfig:{},activeFieldId:null,_internalUpdate:!1,_appliedHash:"",_fieldsDirty:!1,_masterCellNodes:null});const l=(()=>{let t=!!window.__DT_PERF_MODE__;const e={refreshTime:0,refreshCount:0,scrollCount:0,totalNodes:0};return t&&!window.__statePerformanceTelemetry&&(window.__statePerformanceTelemetry={scanCount:0,reuseCount:0,moveCount:0}),{get stats(){return e},enable:()=>{t=!0},disable:()=>{t=!1},onScroll:()=>{t&&e.scrollCount++},startFrame:()=>{var e,n,o;return t?{start:performance.now(),scan:(null==(e=window.__statePerformanceTelemetry)?void 0:e.scanCount)||0,move:(null==(n=window.__statePerformanceTelemetry)?void 0:n.moveCount)||0,reuse:(null==(o=window.__statePerformanceTelemetry)?void 0:o.reuseCount)||0}:null},endFrame:(n,o)=>{if(!t||!n)return;e.refreshCount++,e.totalNodes+=o;const i=performance.now()-n.start;e.refreshTime+=i;const r=window.__statePerformanceTelemetry;if(r){const t=r.scanCount-n.scan,e=r.moveCount-n.move,l=r.reuseCount-n.reuse;(t>0||i>2)&&console.log(`[DataTable Frame] Time: ${i.toFixed(2)}ms, Scans: ${t}, Moves: ${e}, Reuses: ${l}, Rows: ${o}`)}}}})();o.perf=l.stats;const s=((t,e)=>{let n=null,o=null,i=[];const r=(t,e)=>!!(n&&t>=n.minRow&&t<=n.maxRow&&e>=n.minCol&&e<=n.maxCol)||i.some(n=>t>=n.minRow&&t<=n.maxRow&&e>=n.minCol&&e<=n.maxCol);let l=!1;const s=()=>{var o;if(window.__DT_FEATURES__&&!window.__DT_FEATURES__.selection)return;let s=1/0,a=-1/0;n&&(s=Math.min(s,n.minRow),a=Math.max(a,n.maxRow)),i.forEach(t=>{s=Math.min(s,t.minRow),a=Math.max(a,t.maxRow)});const d=s!==1/0;if(!d&&!l)return;l=d;const c=t.querySelector(".dt-body");if(!c)return;const m=c.children;for(let t=0;ta){const t=n.children;for(let e=0;e{let t=0;n&&(t+=n.maxRow-n.minRow+1),i.forEach(e=>t+=e.maxRow-e.minRow+1),e.selectedRowCount=t},d=(t=!1)=>{t||(n=null,o=null,i=[],s(),a())},c=()=>{if(!n)return null;let t=n.minRow,e=n.maxRow,o=n.minCol,r=n.maxCol;return i.forEach(n=>{t=Math.min(t,n.minRow),e=Math.max(e,n.maxRow),o=Math.min(o,n.minCol),r=Math.max(r,n.maxCol)}),{minRow:t,maxRow:e,minCol:o,maxCol:r}};return{applySelectionUI:s,clearAllActive:d,startSelect:(l,c,m)=>{const u=r(l,c),f=n&&(n.minRow!==n.maxRow||n.minCol!==n.maxCol)||i.length>0;m.shiftKey&&o?n={minRow:Math.min(o.row,l),maxRow:Math.max(o.row,l),minCol:Math.min(o.col,c),maxCol:Math.max(o.col,c)}:(!u||m.ctrlKey||m.metaKey?(m.ctrlKey||m.metaKey?n&&!u&&i.push(n):d(),o={row:l,col:c},n={minRow:l,maxRow:l,minCol:c,maxCol:c}):f||(t._potentialCancel={row:l,col:c}),e.isSelecting=!0),s(),a(),t.focus()},updateSelect:(i,r)=>{e.isSelecting&&o&&(n={minRow:Math.min(o.row,i),maxRow:Math.max(o.row,i),minCol:Math.min(o.col,r),maxCol:Math.max(o.col,r)},t._potentialCancel=null,s(),a())},endSelect:()=>{if(t._potentialCancel){const{row:e,col:n}=t._potentialCancel;r(e,n)&&d(),t._potentialCancel=null}e.isSelecting=!1},deleteSelected:()=>{const t=c();if(!t)return;const n=t.minRow,o=t.maxRow,i=e.list.filter((t,e)=>!(e>=n&&e<=o));e.list=i,d()},getSelectionBounds:c,copy:async()=>{const t=c();if(!t)return;const n=e.list.slice(t.minRow,t.maxRow+1).map(n=>e.fields.slice(t.minCol,t.maxCol+1).map(t=>{let e=String(n[t.id]??"");return(e.includes("\t")||e.includes("\n")||e.includes('"'))&&(e='"'+e.replace(/"/g,'""')+'"'),e}).join("\t")).join("\n");await navigator.clipboard.writeText(n)},paste:async()=>{try{const n=await navigator.clipboard.readText();if(!n)return;const o=c();if(!o)return;const i=n.split(/\r?\n/).filter(t=>t.length>0).map(t=>{const e=[];let n="",o=!1;for(let i=0;i{var e;return null==(e=t.classList)?void 0:e.contains("dt-body-row")}):[];i.forEach((n,o)=>{const i=r+o;if(i>s||i>=e.list.length)return;const d=e.list[i];let c=!1;if(n.forEach((t,n)=>{const o=l+n;if(o>a||o>=e.fields.length)return;const i=e.fields[o];d[i.id]=t,c=!0}),c&&t.refreshNode){const n=m.find(t=>{var n;return((null==(n=t._ref)?void 0:n.rIdx)??-1)+e._listStartIndex===i});n&&t.refreshNode(n)}})}catch(t){console.error("Paste Error:",t)}}}})(t,o),a=((t,e,n)=>{const o=i({itemHeight:40});let r=null;const l=(t=!1)=>{if(!r)return;const i=o.calc(r,e.list);if(i){if(!t&&e.prevHeight===i.prevHeight&&e.postHeight===i.postHeight&&e._listStartIndex===i.listStartIndex&&e._renderedList.length===i.renderedList.length)return;Object.assign(e,{prevHeight:i.prevHeight,postHeight:i.postHeight,_listStartIndex:i.listStartIndex,_renderedList:i.renderedList}),null==n||n(i.renderedList.length,t)}};return{init:()=>{r=t.querySelector(".dt-main")},reset:n=>{e._listStartIndex=0,o.reset(n,r||t),e.list===n&&o.init(n,()=>l(!0))},refresh:l,onScroll:()=>l(!1)}})(t,o,(t,e)=>{s.applySelectionUI()});let d,c,m;t.format=(t,e)=>e.formatter?e.formatter(t,e):"string"==typeof t?t:null==t?"":"object"==typeof t?JSON.stringify(t):String(t),t.refreshNode=t=>n(t),t.refresh=()=>{const e=l.startFrame();if(a.refresh(),d||(d=t.querySelector(".dt-spacer-prev")),d){const t=o.prevHeight||0;d.style.height=t+"px",d.style.display=t>0?"block":"none"}if(c||(c=t.querySelector(".dt-spacer-post")),c){const t=o.postHeight||0;c.style.height=t+"px",c.style.display=t>0?"block":"none"}l.endFrame(e,o._renderedList.length)};let u=null;t.onScroll=()=>{l.onScroll(),t.refresh(),u||(u=requestAnimationFrame(()=>{u=null;const e=t.querySelector(".dt-column-menu");if(e&&"none"!==e.style.display&&o.activeFieldId){const n=t.querySelector(`.header-cell[data-id="${o.activeFieldId}"]`);if(n){const o=n.querySelector(".header-menu-btn").getBoundingClientRect(),i=t.getBoundingClientRect();e.style.left=o.right-i.left-180+"px",e.style.top=o.bottom-i.top+5+"px"}}}))},t.applySortFilter=(t={})=>{if(o._internalUpdate)return;const e={...o.filterConfig,...t.filters||{}},n=void 0!==t.sort?t.sort?{fieldId:o.activeFieldId,direction:t.sort}:{fieldId:null,direction:null}:o.sortConfig,i=JSON.stringify({s:n,f:e});if(o._appliedHash===i&&!t.force)return;o._internalUpdate=!0;const r=performance.now();let l=t.force?o.list:[...o._originalList||[]];t.force||(Object.keys(e).forEach(t=>{const n=e[t];if(n){const e=String(n).toLowerCase();l=l.filter(n=>String(n[t]??"").toLowerCase().includes(e))}}),n&&n.fieldId&&n.direction&&l.sort((t,e)=>{let o=t[n.fieldId],i=e[n.fieldId];if(o===i)return 0;if(null==o)return 1;if(null==i)return-1;const r=o>i?1:-1;return"asc"===n.direction?r:-r})),window.__perfTrace={evalCount:0,evalTotal:0},performance.now(),o._appliedHash=i,void 0!==t.sort&&(o.sortConfig=n),o.list=l,o._internalUpdate=!1;const s=performance.now()-r;console.log(`[DataTable Performance Profile] Sync Block: ${s.toFixed(2)}ms (Eval: ${window.__perfTrace.evalCount})`),requestAnimationFrame(()=>{setTimeout(()=>{console.log(`[DataTable Performance Profile] E2E Paint: ${(performance.now()-r).toFixed(2)}ms`)})}),window.__perfTrace=null},t.showColumnMenu=(e,i)=>{i.stopPropagation();const r=i.currentTarget,l=r.getBoundingClientRect(),s=t.getBoundingClientRect(),a=t.querySelector(".dt-column-menu");o.activeFieldId=e.id,a.style.display="block",a.style.left=l.right-s.left-180+"px",a.style.top=l.bottom-s.top+5+"px";const d=e=>{a.contains(e.target)||r.contains(e.target)||(t.hideColumnMenu(),t.applySortFilter(),document.removeEventListener("mousedown",d))};document.addEventListener("mousedown",d),n(a),setTimeout(()=>{const t=a.querySelector("input");t&&t.focus()},50)},t.hideColumnMenu=()=>{const e=t.querySelector(".dt-column-menu");e&&(e.style.display="none")},t.setSort=e=>{t.applySortFilter({sort:e})},t.clearColumnSettings=()=>{const e={...o.filterConfig};delete e[o.activeFieldId],o.filterConfig=e,t.applySortFilter({sort:null}),t.hideColumnMenu()},t._initRow=t=>{if(!t||!t.children)return;const e=t.children;for(let t=0;t{var n,i;const r=e.target.closest(".dt-cell");if(!r)return;const l=r.closest(".dt-row");if(!l||l.classList.contains("dt-header-row"))return;const s=void 0!==r.dataset.fidx?parseInt(r.dataset.fidx):(null==(n=r._ref)?void 0:n.fIdx)??Array.from(l.children).indexOf(r),a=(null==(i=l._ref)?void 0:i.rIdx)??Array.from(t.querySelectorAll(".dt-body-row")).indexOf(l);t.startSelect(a+o._listStartIndex,s,e)},t.onMainMouseOver=e=>{var n,i;if(!o.isSelecting)return;const r=e.target.closest(".dt-cell");if(!r)return;const l=r.closest(".dt-row");if(!l||l.classList.contains("dt-header-row"))return;const s=void 0!==r.dataset.fidx?parseInt(r.dataset.fidx):(null==(n=r._ref)?void 0:n.fIdx)??Array.from(l.children).indexOf(r),a=(null==(i=l._ref)?void 0:i.rIdx)??Array.from(t.querySelectorAll(".dt-body-row")).indexOf(l);t.updateSelect(a+o._listStartIndex,s)},t.onMainDblClick=e=>{var n,i,r;const l=e.target.closest(".dt-cell");if(!l)return;const s=l.closest(".dt-row");if(!s||s.classList.contains("dt-header-row"))return;const a=null==(n=s._ref)?void 0:n.item,d=void 0!==l.dataset.fidx?parseInt(l.dataset.fidx):(null==(i=l._ref)?void 0:i.fIdx)??Array.from(s.children).indexOf(l),c=(null==(r=l._ref)?void 0:r.f)??o.fields[d];a&&c&&t.editCell(a,c,l)},o.__watch("fields",e=>{if(!e)return;o._fieldsDirty=!0;const n=e.map(t=>`var(--w-${t.id}, ${t.width||150}px)`).join(" "),i=e.reduce((t,e)=>t+(e.width||150),0);t.style.setProperty("--dt-grid-template",n),t.style.setProperty("--dt-row-width",i+"px")}),o.__watch("list",e=>{if(o._fieldsDirty){o._fieldsDirty=!1;const e=t.querySelector('.dt-body template[index="rIdx"]');if(e){const t=e.content.querySelector('template[as="f"]');t&&(o._masterCellNodes||(o._masterCellNodes=Array.from(t.content.childNodes).map(t=>t.cloneNode(!0))),t.removeAttribute("$each"),t.removeAttribute("as"),t.removeAttribute("index"),t.setAttribute("$if","true"),t.content.textContent="",o.fields.forEach((e,n)=>{o._masterCellNodes.forEach(e=>{const o=e.cloneNode(!0);1===o.nodeType&&(o.dataset.fidx=n),t.content.appendChild(o)})}))}}o._internalUpdate||(o._originalList=[...e||[]]),a.init(),a.reset(e)});let f=null;t.editCell=(e,o,i)=>{const l=t.querySelector(".dt-editor-overlay"),s=l.querySelector("AutoForm"),a=i.getBoundingClientRect(),d=t.getBoundingClientRect();f=i;let c=a.width;"textarea"===o.type||"TagsInput"===o.type?c=Math.max(a.width,300):"radio"===o.type&&(c=Math.max(a.width,240)),l.style.display="flex",l.style.left=a.left-d.left+"px",l.style.top=a.top-d.top+"px",l.style.width=c+"px",l.style.height="textarea"===o.type||"TagsInput"===o.type?"auto":a.height+"px",r.editingSchema=[{...o,name:o.id,label:""}],r.editingData=e,n(l),setTimeout(()=>{const t=s.querySelector(".form-control, .form-select, .form-check-input, input");t&&t.focus()},30)},t.hideEditor=(e=!0)=>{m||(m=t.querySelector(".dt-editor-overlay")),m&&"none"!==m.style.display&&(m.style.display="none",e&&f&&n(f),r.editingSchema=r.editingData=f=null,t.focus())},t.startSelect=s.startSelect,t.updateSelect=s.updateSelect,t.deleteSelected=s.deleteSelected;const p=e=>{const n=t.querySelector(".dt-editor-overlay");n&&"none"!==n.style.display&&!n.contains(e.target)&&t.hideEditor(!0),t.contains(e.target)||(null==n?void 0:n.contains(e.target))||s.clearAllActive()};window.addEventListener("mouseup",s.endSelect),document.addEventListener("mousedown",p),t._onUnload=()=>{document.removeEventListener("mousedown",p),window.removeEventListener("mouseup",s.endSelect)}},o.makeDom('\n\n\t
\n\t\t\n\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t
\n\t\t\t\n\t\t\t
\n\t\t
\n\t
\n\n\t
\n\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t LOCAL FILTER\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t\t
\n\t\t
\n\t
\n\n\t
\n\n\t\n
\n'),o.makeDom("\n\n")),"undefined"!=typeof document&&n(document.documentElement);
diff --git a/src/index.js b/src/index.js
index e0781ea..6519be7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,7 +11,15 @@ Component.register('DataTable', container => {
Object.assign(state, {
list: [], fields: [], _renderedList: [],
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();
@@ -19,10 +27,7 @@ Component.register('DataTable', container => {
const selection = createSelectionManager(container, state);
- const scroll = createScrollManager(container, state, (renderedCount) => {
- // Only hide editor if we actually scroll past its visibility range
- // For now, simple hide for safety
- container.hideEditor();
+ const scroll = createScrollManager(container, state, (renderedCount, isLayoutChange) => {
selection.applySelectionUI();
});
@@ -41,7 +46,6 @@ Component.register('DataTable', container => {
const frameStart = perf.startFrame();
scroll.refresh();
- // Manual DOM Sync for Spacers (Authorized optimization to avoid $if/$style overhead)
if (!_prevSpacer) _prevSpacer = container.querySelector('.dt-spacer-prev');
if (_prevSpacer) {
const h = state.prevHeight || 0;
@@ -58,22 +62,143 @@ Component.register('DataTable', container => {
perf.endFrame(frameStart, state._renderedList.length);
};
+ let _scrollRaf = null;
container.onScroll = () => {
perf.onScroll();
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 => {
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 = cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell);
+ const fIdx = cell.dataset.fidx !== undefined ? parseInt(cell.dataset.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 absoluteRow = rIdx + state._listStartIndex;
- container.startSelect(absoluteRow, fIdx, e);
+ container.startSelect(rIdx + state._listStartIndex, fIdx, e);
};
container.onMainMouseOver = e => {
@@ -82,11 +207,9 @@ Component.register('DataTable', container => {
if (!cell) return;
const row = cell.closest('.dt-row');
if (!row || row.classList.contains('dt-header-row')) return;
-
- const fIdx = cell._ref?.fIdx ?? Array.from(row.children).indexOf(cell);
+ const fIdx = cell.dataset.fidx !== undefined ? parseInt(cell.dataset.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 absoluteRow = rIdx + state._listStartIndex;
- container.updateSelect(absoluteRow, fIdx);
+ container.updateSelect(rIdx + state._listStartIndex, fIdx);
};
container.onMainDblClick = e => {
@@ -94,64 +217,73 @@ Component.register('DataTable', container => {
if (!cell) return;
const row = cell.closest('.dt-row');
if (!row || row.classList.contains('dt-header-row')) return;
-
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);
};
+ 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 => {
+ 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.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;
-
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;
-
- // 1. Precise Positioning with expansion and Z-index
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';
-
- // 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
+ overlay.style.height = (field.type === 'textarea' || field.type === 'TagsInput' ? 'auto' : rect.height + 'px');
State.editingSchema = [{ ...field, name: field.id, label: '' }];
State.editingData = row;
RefreshState(overlay);
-
- // 3. Focus
setTimeout(() => {
const input = editor.querySelector('.form-control, .form-select, .form-check-input, input');
if (input) input.focus();
@@ -161,51 +293,27 @@ Component.register('DataTable', container => {
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;
+ if (save && currentEditingNode) RefreshState(currentEditingNode);
+ State.editingSchema = State.editingData = currentEditingNode = null;
container.focus();
};
- // --- Shared Logic ---
container.startSelect = selection.startSelect;
container.updateSelect = selection.updateSelect;
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 overlay = container.querySelector('.dt-editor-overlay');
- if (overlay && overlay.style.display !== 'none' && !overlay.contains(e.target)) {
- container.hideEditor(true);
- }
+ if (overlay && overlay.style.display !== 'none' && !overlay.contains(e.target)) container.hideEditor(true);
if (!container.contains(e.target) && !overlay?.contains(e.target)) selection.clearAllActive();
}
window.addEventListener('mouseup', selection.endSelect);
document.addEventListener('mousedown', onGlobalMouseDown);
-
container._onUnload = () => {
- document.removeEventListener('mousedown', onGlobalMouseDown)
- window.removeEventListener('mouseup', selection.endSelect)
+ document.removeEventListener('mousedown', onGlobalMouseDown);
+ window.removeEventListener('mouseup', selection.endSelect);
}
}, Util.makeDom(/*html*/`
@@ -217,35 +325,70 @@ Component.register('DataTable', container => {
style="overflow-anchor:none; min-height: 0">
-
-
+
+
+
+
+
+
+
+
+
+
`), Util.makeDom(/*html*/`
@@ -255,57 +398,27 @@ Component.register('DataTable', container => {
.dt-row {
display: grid;
grid-template-columns: var(--dt-grid-template);
- width: max-content;
+ width: var(--dt-row-width, max-content);
min-width: 100%;
height: 40px;
+ contain: paint layout;
}
- .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-header-row { 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-selected { background-color: var(--bs-primary-bg-subtle) !important; }
+ .header-cell .header-menu-btn { opacity: 0; transition: opacity 0.2s; }
+ .header-cell:hover .header-menu-btn { opacity: 1; }
+ .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-column-menu .list-group-item:hover { background-color: var(--bs-light); }
.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;
+ 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;
}
+ .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; }
`))
diff --git a/src/scroll.js b/src/scroll.js
index fe6d40b..4ee263e 100644
--- a/src/scroll.js
+++ b/src/scroll.js
@@ -4,12 +4,13 @@ export const createScrollManager = (container, state, onRenderedListChange) => {
const vs = VirtualScroll({ itemHeight: 40 });
let scrollEl = null;
- const refresh = () => {
+ const refresh = (isLayoutChange = false) => {
if (!scrollEl) return;
const res = vs.calc(scrollEl, state.list);
if (res) {
// Dirty check: Abort if layout boundaries haven't actually changed
- if (state.prevHeight === res.prevHeight &&
+ if (!isLayoutChange &&
+ state.prevHeight === res.prevHeight &&
state.postHeight === res.postHeight &&
state._listStartIndex === res.listStartIndex &&
state._renderedList.length === res.renderedList.length) {
@@ -22,7 +23,7 @@ export const createScrollManager = (container, state, onRenderedListChange) => {
_listStartIndex: res.listStartIndex,
_renderedList: res.renderedList
});
- onRenderedListChange?.(res.renderedList.length);
+ onRenderedListChange?.(res.renderedList.length, isLayoutChange);
}
};
@@ -32,12 +33,14 @@ export const createScrollManager = (container, state, onRenderedListChange) => {
},
reset: (list) => {
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) {
- vs.init(list, refresh);
+ vs.init(list, () => refresh(true));
}
},
refresh,
- onScroll: refresh
+ onScroll: () => refresh(false)
};
};
diff --git a/src/selection.js b/src/selection.js
index 122ef26..8885f9f 100644
--- a/src/selection.js
+++ b/src/selection.js
@@ -11,6 +11,9 @@ export const createSelectionManager = (container, state) => {
let lastHadSelection = false;
const applySelectionUI = () => {
+ /* DEBUG: Performance Isolation Toggle */
+ if (window.__DT_FEATURES__ && !window.__DT_FEATURES__.selection) return;
+
let boundMinRow = Infinity, boundMaxRow = -Infinity;
if (activeBounds) {
boundMinRow = Math.min(boundMinRow, activeBounds.minRow);
@@ -22,41 +25,35 @@ export const createSelectionManager = (container, state) => {
});
const hasSelection = boundMinRow !== Infinity;
-
- // Fast path: If no current selection AND last frame also had no selection, skip EVERYTHING.
if (!hasSelection && !lastHadSelection) return;
lastHadSelection = hasSelection;
const body = container.querySelector('.dt-body');
if (!body) return;
- const rowNodes = body.children;
- // Fast path for scroll performance: Only iterate if selection overlaps or we need to clear
- for (let r = 0; r < rowNodes.length; r++) {
- const rowNode = rowNodes[r];
+ // Fast path: Use children instead of querySelectorAll
+ const nodes = body.children;
+ for (let r = 0; r < nodes.length; r++) {
+ const rowNode = nodes[r];
if (!rowNode.classList.contains('dt-body-row')) continue;
const absoluteRow = (rowNode._ref?.rIdx ?? -1) + state._listStartIndex;
- const cellNodes = rowNode.children;
- // Fast path: Row completely outside selection
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++) {
- if (cellNodes[i].classList.contains('dt-cell-selected')) {
- cellNodes[i].classList.remove('dt-cell-selected');
- }
+ cellNodes[i].classList.remove('dt-cell-selected');
}
continue;
}
+ const cellNodes = rowNode.children;
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');
- }
+ const cell = cellNodes[i];
+ if (isSelected) cell.classList.add('dt-cell-selected');
+ else cell.classList.remove('dt-cell-selected');
}
}
};
diff --git a/test/index.html b/test/index.html
index d6cd26e..548b0a1 100644
--- a/test/index.html
+++ b/test/index.html
@@ -6,6 +6,7 @@
DataTable Demo
+