@alaarab/ogrid-js 2.1.15 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js
CHANGED
|
@@ -710,6 +710,66 @@ function calculateDropTarget(params) {
|
|
|
710
710
|
}
|
|
711
711
|
return { targetIndex, indicatorX };
|
|
712
712
|
}
|
|
713
|
+
function computeVisibleColumnRange(scrollLeft, columnWidths, containerWidth, overscan = 2) {
|
|
714
|
+
if (columnWidths.length === 0 || containerWidth <= 0) {
|
|
715
|
+
return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
|
|
716
|
+
}
|
|
717
|
+
let cumWidth = 0;
|
|
718
|
+
let rawStart = columnWidths.length;
|
|
719
|
+
let rawEnd = -1;
|
|
720
|
+
for (let i = 0; i < columnWidths.length; i++) {
|
|
721
|
+
const colStart = cumWidth;
|
|
722
|
+
cumWidth += columnWidths[i];
|
|
723
|
+
if (cumWidth > scrollLeft && rawStart === columnWidths.length) {
|
|
724
|
+
rawStart = i;
|
|
725
|
+
}
|
|
726
|
+
if (colStart < scrollLeft + containerWidth) {
|
|
727
|
+
rawEnd = i;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (rawStart > rawEnd) {
|
|
731
|
+
return { startIndex: 0, endIndex: -1, leftOffset: 0, rightOffset: 0 };
|
|
732
|
+
}
|
|
733
|
+
const startIndex = Math.max(0, rawStart - overscan);
|
|
734
|
+
const endIndex = Math.min(columnWidths.length - 1, rawEnd + overscan);
|
|
735
|
+
let leftOffset = 0;
|
|
736
|
+
for (let i = 0; i < startIndex; i++) {
|
|
737
|
+
leftOffset += columnWidths[i];
|
|
738
|
+
}
|
|
739
|
+
let rightOffset = 0;
|
|
740
|
+
for (let i = endIndex + 1; i < columnWidths.length; i++) {
|
|
741
|
+
rightOffset += columnWidths[i];
|
|
742
|
+
}
|
|
743
|
+
return { startIndex, endIndex, leftOffset, rightOffset };
|
|
744
|
+
}
|
|
745
|
+
function partitionColumnsForVirtualization(visibleCols, columnRange, pinnedColumns) {
|
|
746
|
+
const pinnedLeft = [];
|
|
747
|
+
const pinnedRight = [];
|
|
748
|
+
const unpinned = [];
|
|
749
|
+
for (const col of visibleCols) {
|
|
750
|
+
const pin = pinnedColumns?.[col.columnId];
|
|
751
|
+
if (pin === "left") pinnedLeft.push(col);
|
|
752
|
+
else if (pin === "right") pinnedRight.push(col);
|
|
753
|
+
else unpinned.push(col);
|
|
754
|
+
}
|
|
755
|
+
if (!columnRange || columnRange.endIndex < 0) {
|
|
756
|
+
return {
|
|
757
|
+
pinnedLeft,
|
|
758
|
+
virtualizedUnpinned: unpinned,
|
|
759
|
+
pinnedRight,
|
|
760
|
+
leftSpacerWidth: 0,
|
|
761
|
+
rightSpacerWidth: 0
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const virtualizedUnpinned = unpinned.slice(columnRange.startIndex, columnRange.endIndex + 1);
|
|
765
|
+
return {
|
|
766
|
+
pinnedLeft,
|
|
767
|
+
virtualizedUnpinned,
|
|
768
|
+
pinnedRight,
|
|
769
|
+
leftSpacerWidth: columnRange.leftOffset,
|
|
770
|
+
rightSpacerWidth: columnRange.rightOffset
|
|
771
|
+
};
|
|
772
|
+
}
|
|
713
773
|
function computeVisibleRange(scrollTop, rowHeight, containerHeight, totalRows, overscan = 5) {
|
|
714
774
|
if (totalRows <= 0 || rowHeight <= 0 || containerHeight <= 0) {
|
|
715
775
|
return { startIndex: 0, endIndex: 0, offsetTop: 0, offsetBottom: 0 };
|
|
@@ -737,6 +797,235 @@ function getScrollTopForRow(rowIndex, rowHeight, containerHeight, align = "start
|
|
|
737
797
|
return Math.max(0, rowTop - containerHeight + rowHeight);
|
|
738
798
|
}
|
|
739
799
|
}
|
|
800
|
+
function workerBody() {
|
|
801
|
+
const ctx = self;
|
|
802
|
+
ctx.onmessage = (e) => {
|
|
803
|
+
const msg = e.data;
|
|
804
|
+
if (msg.type !== "sort-filter") return;
|
|
805
|
+
const { requestId, values, filters, sort } = msg;
|
|
806
|
+
const rowCount = values.length;
|
|
807
|
+
let indices = [];
|
|
808
|
+
const filterEntries = Object.entries(filters);
|
|
809
|
+
if (filterEntries.length === 0) {
|
|
810
|
+
indices = new Array(rowCount);
|
|
811
|
+
for (let i = 0; i < rowCount; i++) indices[i] = i;
|
|
812
|
+
} else {
|
|
813
|
+
for (let r = 0; r < rowCount; r++) {
|
|
814
|
+
let pass = true;
|
|
815
|
+
for (let f = 0; f < filterEntries.length; f++) {
|
|
816
|
+
const colIdx = Number(filterEntries[f][0]);
|
|
817
|
+
const filter = filterEntries[f][1];
|
|
818
|
+
const cellVal = values[r][colIdx];
|
|
819
|
+
switch (filter.type) {
|
|
820
|
+
case "text": {
|
|
821
|
+
const trimmed = filter.value.trim().toLowerCase();
|
|
822
|
+
if (trimmed && !String(cellVal ?? "").toLowerCase().includes(trimmed)) {
|
|
823
|
+
pass = false;
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case "multiSelect": {
|
|
828
|
+
if (filter.value.length > 0) {
|
|
829
|
+
const set = new Set(filter.value);
|
|
830
|
+
if (!set.has(String(cellVal ?? ""))) {
|
|
831
|
+
pass = false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "date": {
|
|
837
|
+
if (cellVal == null) {
|
|
838
|
+
pass = false;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
const ts = new Date(String(cellVal)).getTime();
|
|
842
|
+
if (isNaN(ts)) {
|
|
843
|
+
pass = false;
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
if (filter.value.from) {
|
|
847
|
+
const fromTs = (/* @__PURE__ */ new Date(filter.value.from + "T00:00:00")).getTime();
|
|
848
|
+
if (ts < fromTs) {
|
|
849
|
+
pass = false;
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (filter.value.to) {
|
|
854
|
+
const toTs = (/* @__PURE__ */ new Date(filter.value.to + "T23:59:59.999")).getTime();
|
|
855
|
+
if (ts > toTs) {
|
|
856
|
+
pass = false;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (!pass) break;
|
|
864
|
+
}
|
|
865
|
+
if (pass) indices.push(r);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (sort) {
|
|
869
|
+
const { columnIndex, direction } = sort;
|
|
870
|
+
const dir = direction === "asc" ? 1 : -1;
|
|
871
|
+
indices.sort((a, b) => {
|
|
872
|
+
const av = values[a][columnIndex];
|
|
873
|
+
const bv = values[b][columnIndex];
|
|
874
|
+
if (av == null && bv == null) return 0;
|
|
875
|
+
if (av == null) return -1 * dir;
|
|
876
|
+
if (bv == null) return 1 * dir;
|
|
877
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
878
|
+
return av === bv ? 0 : av > bv ? dir : -dir;
|
|
879
|
+
}
|
|
880
|
+
const sa = String(av).toLowerCase();
|
|
881
|
+
const sb = String(bv).toLowerCase();
|
|
882
|
+
return sa === sb ? 0 : sa > sb ? dir : -dir;
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
const response = {
|
|
886
|
+
type: "sort-filter-result",
|
|
887
|
+
requestId,
|
|
888
|
+
indices
|
|
889
|
+
};
|
|
890
|
+
ctx.postMessage(response);
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
var workerInstance = null;
|
|
894
|
+
var requestCounter = 0;
|
|
895
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
896
|
+
function createSortFilterWorker() {
|
|
897
|
+
if (workerInstance) return workerInstance;
|
|
898
|
+
if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined") {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
const fnStr = workerBody.toString();
|
|
903
|
+
const blob = new Blob(
|
|
904
|
+
[`(${fnStr})()`],
|
|
905
|
+
{ type: "application/javascript" }
|
|
906
|
+
);
|
|
907
|
+
const url = URL.createObjectURL(blob);
|
|
908
|
+
workerInstance = new Worker(url);
|
|
909
|
+
URL.revokeObjectURL(url);
|
|
910
|
+
workerInstance.onmessage = (e) => {
|
|
911
|
+
const { requestId, indices } = e.data;
|
|
912
|
+
const pending = pendingRequests.get(requestId);
|
|
913
|
+
if (pending) {
|
|
914
|
+
pendingRequests.delete(requestId);
|
|
915
|
+
pending.resolve(indices);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
workerInstance.onerror = (err) => {
|
|
919
|
+
for (const [id, pending] of pendingRequests) {
|
|
920
|
+
pending.reject(new Error(err.message || "Worker error"));
|
|
921
|
+
pendingRequests.delete(id);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
return workerInstance;
|
|
925
|
+
} catch {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function terminateSortFilterWorker() {
|
|
930
|
+
if (workerInstance) {
|
|
931
|
+
workerInstance.terminate();
|
|
932
|
+
workerInstance = null;
|
|
933
|
+
}
|
|
934
|
+
for (const [id, pending] of pendingRequests) {
|
|
935
|
+
pending.reject(new Error("Worker terminated"));
|
|
936
|
+
pendingRequests.delete(id);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function extractValueMatrix(data, columns) {
|
|
940
|
+
const matrix = new Array(data.length);
|
|
941
|
+
for (let r = 0; r < data.length; r++) {
|
|
942
|
+
const row = new Array(columns.length);
|
|
943
|
+
for (let c = 0; c < columns.length; c++) {
|
|
944
|
+
const val = getCellValue(data[r], columns[c]);
|
|
945
|
+
if (val == null) {
|
|
946
|
+
row[c] = null;
|
|
947
|
+
} else if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
|
|
948
|
+
row[c] = val;
|
|
949
|
+
} else {
|
|
950
|
+
row[c] = String(val);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
matrix[r] = row;
|
|
954
|
+
}
|
|
955
|
+
return matrix;
|
|
956
|
+
}
|
|
957
|
+
function processClientSideDataAsync(data, columns, filters, sortBy, sortDirection) {
|
|
958
|
+
if (sortBy) {
|
|
959
|
+
const sortCol = columns.find((c) => c.columnId === sortBy);
|
|
960
|
+
if (sortCol?.compare) {
|
|
961
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const worker = createSortFilterWorker();
|
|
965
|
+
if (!worker) {
|
|
966
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
967
|
+
}
|
|
968
|
+
const columnIndexMap = /* @__PURE__ */ new Map();
|
|
969
|
+
for (let i = 0; i < columns.length; i++) {
|
|
970
|
+
columnIndexMap.set(columns[i].columnId, i);
|
|
971
|
+
}
|
|
972
|
+
const values = extractValueMatrix(data, columns);
|
|
973
|
+
const columnMeta = columns.map((col, idx) => ({
|
|
974
|
+
type: col.type ?? "text",
|
|
975
|
+
index: idx
|
|
976
|
+
}));
|
|
977
|
+
const workerFilters = {};
|
|
978
|
+
for (const col of columns) {
|
|
979
|
+
const filterKey = getFilterField(col);
|
|
980
|
+
const val = filters[filterKey];
|
|
981
|
+
if (!val) continue;
|
|
982
|
+
const colIdx = columnIndexMap.get(col.columnId);
|
|
983
|
+
if (colIdx === void 0) continue;
|
|
984
|
+
switch (val.type) {
|
|
985
|
+
case "text":
|
|
986
|
+
workerFilters[colIdx] = { type: "text", value: val.value };
|
|
987
|
+
break;
|
|
988
|
+
case "multiSelect":
|
|
989
|
+
workerFilters[colIdx] = { type: "multiSelect", value: val.value };
|
|
990
|
+
break;
|
|
991
|
+
case "date":
|
|
992
|
+
workerFilters[colIdx] = { type: "date", value: { from: val.value.from, to: val.value.to } };
|
|
993
|
+
break;
|
|
994
|
+
// 'people' filter has a UserLike object — fall back to sync
|
|
995
|
+
case "people":
|
|
996
|
+
return Promise.resolve(processClientSideData(data, columns, filters, sortBy, sortDirection));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
let sort;
|
|
1000
|
+
if (sortBy) {
|
|
1001
|
+
const sortColIdx = columnIndexMap.get(sortBy);
|
|
1002
|
+
if (sortColIdx !== void 0) {
|
|
1003
|
+
sort = { columnIndex: sortColIdx, direction: sortDirection ?? "asc" };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const requestId = ++requestCounter;
|
|
1007
|
+
return new Promise((resolve, reject) => {
|
|
1008
|
+
pendingRequests.set(requestId, {
|
|
1009
|
+
resolve: (indices) => {
|
|
1010
|
+
const result = new Array(indices.length);
|
|
1011
|
+
for (let i = 0; i < indices.length; i++) {
|
|
1012
|
+
result[i] = data[indices[i]];
|
|
1013
|
+
}
|
|
1014
|
+
resolve(result);
|
|
1015
|
+
},
|
|
1016
|
+
reject
|
|
1017
|
+
});
|
|
1018
|
+
const request = {
|
|
1019
|
+
type: "sort-filter",
|
|
1020
|
+
requestId,
|
|
1021
|
+
values,
|
|
1022
|
+
columnMeta,
|
|
1023
|
+
filters: workerFilters,
|
|
1024
|
+
sort
|
|
1025
|
+
};
|
|
1026
|
+
worker.postMessage(request);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
740
1029
|
function getHeaderFilterConfig(col, input) {
|
|
741
1030
|
const filterable = isFilterConfig(col.filterable) ? col.filterable : null;
|
|
742
1031
|
const filterType = filterable?.type ?? "none";
|
|
@@ -1667,6 +1956,7 @@ var GridState = class {
|
|
|
1667
1956
|
this._ariaLabel = options.ariaLabel;
|
|
1668
1957
|
this._stickyHeader = options.stickyHeader ?? true;
|
|
1669
1958
|
this._fullScreen = options.fullScreen ?? false;
|
|
1959
|
+
this._workerSort = options.workerSort ?? false;
|
|
1670
1960
|
if (!this._dataSource) {
|
|
1671
1961
|
this._filterOptions = deriveFilterOptionsFromData(
|
|
1672
1962
|
this._data,
|
|
@@ -1770,6 +2060,31 @@ var GridState = class {
|
|
|
1770
2060
|
const items = filtered.slice(startIdx, endIdx);
|
|
1771
2061
|
return { items, totalCount };
|
|
1772
2062
|
}
|
|
2063
|
+
/** Whether worker sort should be used for the current data set. */
|
|
2064
|
+
get useWorkerSort() {
|
|
2065
|
+
return this._workerSort === true || this._workerSort === "auto" && this._data.length > 5e3;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Async version of getProcessedItems that offloads sort/filter to a Web Worker.
|
|
2069
|
+
* Falls back to sync when worker sort is not active.
|
|
2070
|
+
*/
|
|
2071
|
+
async getProcessedItemsAsync() {
|
|
2072
|
+
if (this.isServerSide || !this.useWorkerSort) {
|
|
2073
|
+
return this.getProcessedItems();
|
|
2074
|
+
}
|
|
2075
|
+
const filtered = await processClientSideDataAsync(
|
|
2076
|
+
this._data,
|
|
2077
|
+
this._columns,
|
|
2078
|
+
this._filters,
|
|
2079
|
+
this._sort?.field,
|
|
2080
|
+
this._sort?.direction
|
|
2081
|
+
);
|
|
2082
|
+
const totalCount = filtered.length;
|
|
2083
|
+
const startIdx = (this._page - 1) * this._pageSize;
|
|
2084
|
+
const endIdx = startIdx + this._pageSize;
|
|
2085
|
+
const items = filtered.slice(startIdx, endIdx);
|
|
2086
|
+
return { items, totalCount };
|
|
2087
|
+
}
|
|
1773
2088
|
// --- Server-side fetch ---
|
|
1774
2089
|
fetchServerData() {
|
|
1775
2090
|
if (!this._dataSource) return;
|
|
@@ -2159,6 +2474,9 @@ var TableRenderer = class {
|
|
|
2159
2474
|
this.table = document.createElement("table");
|
|
2160
2475
|
this.table.className = "ogrid-table";
|
|
2161
2476
|
this.table.setAttribute("role", "grid");
|
|
2477
|
+
if (this.virtualScrollState) {
|
|
2478
|
+
this.table.setAttribute("data-virtual-scroll", "");
|
|
2479
|
+
}
|
|
2162
2480
|
this.thead = document.createElement("thead");
|
|
2163
2481
|
if (this.state.stickyHeader) {
|
|
2164
2482
|
this.thead.classList.add("ogrid-sticky-header");
|
|
@@ -2571,6 +2889,23 @@ var TableRenderer = class {
|
|
|
2571
2889
|
this.tbody.appendChild(topSpacer);
|
|
2572
2890
|
}
|
|
2573
2891
|
}
|
|
2892
|
+
const colVirtActive = vs?.columnVirtualizationEnabled === true && vs.columnRange != null;
|
|
2893
|
+
let renderCols = visibleCols;
|
|
2894
|
+
let colGlobalIndexMap = null;
|
|
2895
|
+
let colLeftSpacerWidth = 0;
|
|
2896
|
+
let colRightSpacerWidth = 0;
|
|
2897
|
+
if (colVirtActive && vs) {
|
|
2898
|
+
const partition = partitionColumnsForVirtualization(
|
|
2899
|
+
visibleCols,
|
|
2900
|
+
vs.columnRange,
|
|
2901
|
+
this.interactionState?.pinnedColumns
|
|
2902
|
+
);
|
|
2903
|
+
const combined = [...partition.pinnedLeft, ...partition.virtualizedUnpinned, ...partition.pinnedRight];
|
|
2904
|
+
colGlobalIndexMap = combined.map((c) => visibleCols.indexOf(c));
|
|
2905
|
+
renderCols = combined;
|
|
2906
|
+
colLeftSpacerWidth = partition.leftSpacerWidth;
|
|
2907
|
+
colRightSpacerWidth = partition.rightSpacerWidth;
|
|
2908
|
+
}
|
|
2574
2909
|
for (let rowIndex = startIndex; rowIndex <= endIndex; rowIndex++) {
|
|
2575
2910
|
const item = items[rowIndex];
|
|
2576
2911
|
if (!item) continue;
|
|
@@ -2610,9 +2945,18 @@ var TableRenderer = class {
|
|
|
2610
2945
|
td.textContent = String(rowNumberOffset + rowIndex + 1);
|
|
2611
2946
|
tr.appendChild(td);
|
|
2612
2947
|
}
|
|
2613
|
-
|
|
2614
|
-
const
|
|
2615
|
-
|
|
2948
|
+
if (colLeftSpacerWidth > 0) {
|
|
2949
|
+
const spacerTd = document.createElement("td");
|
|
2950
|
+
spacerTd.style.width = `${colLeftSpacerWidth}px`;
|
|
2951
|
+
spacerTd.style.minWidth = `${colLeftSpacerWidth}px`;
|
|
2952
|
+
spacerTd.style.padding = "0";
|
|
2953
|
+
spacerTd.style.border = "none";
|
|
2954
|
+
spacerTd.setAttribute("aria-hidden", "true");
|
|
2955
|
+
tr.appendChild(spacerTd);
|
|
2956
|
+
}
|
|
2957
|
+
for (let colIndex = 0; colIndex < renderCols.length; colIndex++) {
|
|
2958
|
+
const col = renderCols[colIndex];
|
|
2959
|
+
const globalColIndex = (colGlobalIndexMap ? colGlobalIndexMap[colIndex] : colIndex) + colOffset;
|
|
2616
2960
|
const td = document.createElement("td");
|
|
2617
2961
|
td.className = "ogrid-cell";
|
|
2618
2962
|
td.setAttribute("data-column-id", col.columnId);
|
|
@@ -2680,6 +3024,15 @@ var TableRenderer = class {
|
|
|
2680
3024
|
}
|
|
2681
3025
|
tr.appendChild(td);
|
|
2682
3026
|
}
|
|
3027
|
+
if (colRightSpacerWidth > 0) {
|
|
3028
|
+
const spacerTd = document.createElement("td");
|
|
3029
|
+
spacerTd.style.width = `${colRightSpacerWidth}px`;
|
|
3030
|
+
spacerTd.style.minWidth = `${colRightSpacerWidth}px`;
|
|
3031
|
+
spacerTd.style.padding = "0";
|
|
3032
|
+
spacerTd.style.border = "none";
|
|
3033
|
+
spacerTd.setAttribute("aria-hidden", "true");
|
|
3034
|
+
tr.appendChild(spacerTd);
|
|
3035
|
+
}
|
|
2683
3036
|
this.tbody.appendChild(tr);
|
|
2684
3037
|
}
|
|
2685
3038
|
if (isVirtual && vs) {
|
|
@@ -4026,6 +4379,14 @@ var VirtualScrollState = class {
|
|
|
4026
4379
|
this._ro = null;
|
|
4027
4380
|
this._resizeRafId = 0;
|
|
4028
4381
|
this._cachedRange = { startIndex: 0, endIndex: -1, offsetTop: 0, offsetBottom: 0 };
|
|
4382
|
+
// Column virtualization
|
|
4383
|
+
this._scrollLeft = 0;
|
|
4384
|
+
this._scrollLeftRafId = 0;
|
|
4385
|
+
this._containerWidth = 0;
|
|
4386
|
+
this._columnWidths = [];
|
|
4387
|
+
this._cachedColumnRange = null;
|
|
4388
|
+
this._roWidth = null;
|
|
4389
|
+
this._resizeWidthRafId = 0;
|
|
4029
4390
|
this._config = config ?? { enabled: false };
|
|
4030
4391
|
validateVirtualScrollConfig(this._config);
|
|
4031
4392
|
}
|
|
@@ -4054,6 +4415,71 @@ var VirtualScrollState = class {
|
|
|
4054
4415
|
get totalHeight() {
|
|
4055
4416
|
return computeTotalHeight(this._totalRows, this._config.rowHeight ?? DEFAULT_ROW_HEIGHT);
|
|
4056
4417
|
}
|
|
4418
|
+
/** Whether column virtualization is active. */
|
|
4419
|
+
get columnVirtualizationEnabled() {
|
|
4420
|
+
return this._config.columns === true;
|
|
4421
|
+
}
|
|
4422
|
+
/** Get the current visible column range (null when column virtualization is disabled). */
|
|
4423
|
+
get columnRange() {
|
|
4424
|
+
return this._cachedColumnRange;
|
|
4425
|
+
}
|
|
4426
|
+
/** Set the unpinned column widths for horizontal virtualization. */
|
|
4427
|
+
setColumnWidths(widths) {
|
|
4428
|
+
this._columnWidths = widths;
|
|
4429
|
+
this.recomputeColumnRange();
|
|
4430
|
+
}
|
|
4431
|
+
/** Handle horizontal scroll events. RAF-throttled. */
|
|
4432
|
+
handleHorizontalScroll(scrollLeft) {
|
|
4433
|
+
if (!this.columnVirtualizationEnabled) return;
|
|
4434
|
+
if (this._scrollLeftRafId) cancelAnimationFrame(this._scrollLeftRafId);
|
|
4435
|
+
this._scrollLeftRafId = requestAnimationFrame(() => {
|
|
4436
|
+
this._scrollLeftRafId = 0;
|
|
4437
|
+
this._scrollLeft = scrollLeft;
|
|
4438
|
+
this.recomputeColumnRange();
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
/** Observe a container element for width changes (column virtualization). */
|
|
4442
|
+
observeContainerWidth(el) {
|
|
4443
|
+
this.disconnectWidthObserver();
|
|
4444
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
4445
|
+
this._roWidth = new ResizeObserver((entries) => {
|
|
4446
|
+
if (entries.length === 0) return;
|
|
4447
|
+
this._containerWidth = entries[0].contentRect.width;
|
|
4448
|
+
if (this._resizeWidthRafId) cancelAnimationFrame(this._resizeWidthRafId);
|
|
4449
|
+
this._resizeWidthRafId = requestAnimationFrame(() => {
|
|
4450
|
+
this._resizeWidthRafId = 0;
|
|
4451
|
+
this.recomputeColumnRange();
|
|
4452
|
+
});
|
|
4453
|
+
});
|
|
4454
|
+
this._roWidth.observe(el);
|
|
4455
|
+
}
|
|
4456
|
+
this._containerWidth = el.clientWidth;
|
|
4457
|
+
}
|
|
4458
|
+
disconnectWidthObserver() {
|
|
4459
|
+
if (this._roWidth) {
|
|
4460
|
+
this._roWidth.disconnect();
|
|
4461
|
+
this._roWidth = null;
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
/** Recompute visible column range and emit if changed. */
|
|
4465
|
+
recomputeColumnRange() {
|
|
4466
|
+
if (!this.columnVirtualizationEnabled || this._columnWidths.length === 0 || this._containerWidth <= 0) {
|
|
4467
|
+
if (this._cachedColumnRange !== null) {
|
|
4468
|
+
this._cachedColumnRange = null;
|
|
4469
|
+
this.emitter.emit("columnRangeChanged", { columnRange: null });
|
|
4470
|
+
}
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4473
|
+
const overscan = this._config.columnOverscan ?? 2;
|
|
4474
|
+
const newRange = computeVisibleColumnRange(this._scrollLeft, this._columnWidths, this._containerWidth, overscan);
|
|
4475
|
+
const prev = this._cachedColumnRange;
|
|
4476
|
+
if (!prev || prev.startIndex !== newRange.startIndex || prev.endIndex !== newRange.endIndex) {
|
|
4477
|
+
this._cachedColumnRange = newRange;
|
|
4478
|
+
this.emitter.emit("columnRangeChanged", { columnRange: newRange });
|
|
4479
|
+
} else {
|
|
4480
|
+
this._cachedColumnRange = newRange;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4057
4483
|
/** Handle scroll events from the table container. RAF-throttled. */
|
|
4058
4484
|
handleScroll(scrollTop) {
|
|
4059
4485
|
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
@@ -4130,6 +4556,10 @@ var VirtualScrollState = class {
|
|
|
4130
4556
|
this.emitter.on("rangeChanged", handler);
|
|
4131
4557
|
return () => this.emitter.off("rangeChanged", handler);
|
|
4132
4558
|
}
|
|
4559
|
+
onColumnRangeChanged(handler) {
|
|
4560
|
+
this.emitter.on("columnRangeChanged", handler);
|
|
4561
|
+
return () => this.emitter.off("columnRangeChanged", handler);
|
|
4562
|
+
}
|
|
4133
4563
|
onConfigChanged(handler) {
|
|
4134
4564
|
this.emitter.on("configChanged", handler);
|
|
4135
4565
|
return () => this.emitter.off("configChanged", handler);
|
|
@@ -4137,7 +4567,10 @@ var VirtualScrollState = class {
|
|
|
4137
4567
|
destroy() {
|
|
4138
4568
|
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
4139
4569
|
if (this._resizeRafId) cancelAnimationFrame(this._resizeRafId);
|
|
4570
|
+
if (this._scrollLeftRafId) cancelAnimationFrame(this._scrollLeftRafId);
|
|
4571
|
+
if (this._resizeWidthRafId) cancelAnimationFrame(this._resizeWidthRafId);
|
|
4140
4572
|
this.disconnectObserver();
|
|
4573
|
+
this.disconnectWidthObserver();
|
|
4141
4574
|
this.emitter.removeAllListeners();
|
|
4142
4575
|
}
|
|
4143
4576
|
};
|
|
@@ -6427,16 +6860,25 @@ var OGrid = class {
|
|
|
6427
6860
|
this.renderer.setVirtualScrollState(this.virtualScrollState);
|
|
6428
6861
|
const handleScroll = () => {
|
|
6429
6862
|
this.virtualScrollState?.handleScroll(this.tableContainer.scrollTop);
|
|
6863
|
+
this.virtualScrollState?.handleHorizontalScroll(this.tableContainer.scrollLeft);
|
|
6430
6864
|
};
|
|
6431
6865
|
this.tableContainer.addEventListener("scroll", handleScroll, { passive: true });
|
|
6432
6866
|
this.unsubscribes.push(() => {
|
|
6433
6867
|
this.tableContainer.removeEventListener("scroll", handleScroll);
|
|
6434
6868
|
});
|
|
6869
|
+
if (options.virtualScroll?.columns) {
|
|
6870
|
+
this.virtualScrollState.observeContainerWidth(this.tableContainer);
|
|
6871
|
+
}
|
|
6435
6872
|
this.unsubscribes.push(
|
|
6436
6873
|
this.virtualScrollState.onRangeChanged(() => {
|
|
6437
6874
|
this.renderingHelper.updateRendererInteractionState();
|
|
6438
6875
|
})
|
|
6439
6876
|
);
|
|
6877
|
+
this.unsubscribes.push(
|
|
6878
|
+
this.virtualScrollState.onColumnRangeChanged(() => {
|
|
6879
|
+
this.renderingHelper.updateRendererInteractionState();
|
|
6880
|
+
})
|
|
6881
|
+
);
|
|
6440
6882
|
this.api.scrollToRow = (index, opts) => {
|
|
6441
6883
|
this.virtualScrollState?.scrollToRow(index, this.tableContainer, opts?.align);
|
|
6442
6884
|
};
|
|
@@ -6679,4 +7121,4 @@ var OGrid = class {
|
|
|
6679
7121
|
}
|
|
6680
7122
|
};
|
|
6681
7123
|
|
|
6682
|
-
export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, EventEmitter, FillHandleState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VirtualScrollState, Z_INDEX, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleRange, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, emailParser, escapeCsvValue, exportToCsv, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, injectGlobalStyles, isColumnEditable, isFilterConfig, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, processClientSideData, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, toUserLike, triggerCsvDownload, validateColumns, validateRowIds, validateVirtualScrollConfig };
|
|
7124
|
+
export { AUTOSIZE_EXTRA_PX, AUTOSIZE_MAX_PX, CELL_PADDING, CHECKBOX_COLUMN_WIDTH, COLUMN_HEADER_MENU_ITEMS, CellDescriptorCache, ClipboardState, ColumnChooser, ColumnPinningState, ColumnReorderState, ColumnResizeState, ContextMenu, DEFAULT_DEBOUNCE_MS, DEFAULT_MIN_COLUMN_WIDTH, EventEmitter, FillHandleState, GRID_BORDER_RADIUS, GRID_CONTEXT_MENU_ITEMS, GridState, HeaderFilter, HeaderFilterState, InlineCellEditor, KeyboardNavState, MAX_PAGE_BUTTONS, MarchingAntsOverlay, OGrid, OGridEventWiring, OGridRendering, PAGE_SIZE_OPTIONS, PEOPLE_SEARCH_DEBOUNCE_MS, PaginationControls, ROW_NUMBER_COLUMN_WIDTH, RowSelectionState, SIDEBAR_TRANSITION_MS, SelectionState, SideBar, SideBarState, StatusBar, TableLayoutState, TableRenderer, UndoRedoStack, UndoRedoState, VirtualScrollState, Z_INDEX, applyCellDeletion, applyCutClear, applyFillValues, applyPastedValues, applyRangeRowSelection, areGridRowPropsEqual, booleanParser, buildCellIndex, buildCsvHeader, buildCsvRows, buildHeaderRows, buildInlineEditorProps, buildPopoverEditorProps, calculateDropTarget, clampSelectionToBounds, computeAggregations, computeArrowNavigation, computeAutoScrollSpeed, computeNextSortState, computeRowSelectionState, computeTabNavigation, computeTotalHeight, computeVisibleColumnRange, computeVisibleRange, createSortFilterWorker, currencyParser, dateParser, debounce, deriveFilterOptionsFromData, emailParser, escapeCsvValue, exportToCsv, extractValueMatrix, findCtrlArrowTarget, flattenColumns, formatCellValueForTsv, formatSelectionAsTsv, formatShortcut, getCellRenderDescriptor, getCellValue, getColumnHeaderMenuItems, getContextMenuHandlers, getDataGridStatusBarConfig, getFilterField, getHeaderFilterConfig, getMultiSelectFilterFields, getPaginationViewModel, getPinStateForColumn, getScrollTopForRow, getStatusBarParts, injectGlobalStyles, isColumnEditable, isFilterConfig, isInSelectionRange, isRowInRange, measureColumnContentWidth, measureRange, mergeFilter, normalizeSelectionRange, numberParser, parseTsvClipboard, parseValue, partitionColumnsForVirtualization, processClientSideData, processClientSideDataAsync, rangesEqual, reorderColumnArray, resolveCellDisplayContent, resolveCellStyle, terminateSortFilterWorker, toUserLike, triggerCsvDownload, validateColumns, validateRowIds, validateVirtualScrollConfig };
|
package/dist/styles/ogrid.css
CHANGED
|
@@ -329,8 +329,16 @@
|
|
|
329
329
|
border-bottom: 1px solid var(--ogrid-border, #e8e8e8);
|
|
330
330
|
position: relative;
|
|
331
331
|
color: var(--ogrid-fg, #242424);
|
|
332
|
+
contain: content;
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
/* Pinned columns need contain: none because contain breaks position: sticky */
|
|
336
|
+
.ogrid-table td[data-pinned='left'],
|
|
337
|
+
.ogrid-table td[data-pinned='right'] { contain: none; }
|
|
338
|
+
|
|
339
|
+
/* content-visibility: auto on rows for non-virtualized grids */
|
|
340
|
+
.ogrid-table:not([data-virtual-scroll]) tbody tr { content-visibility: auto; }
|
|
341
|
+
|
|
334
342
|
.ogrid-table tbody tr:last-child td {
|
|
335
343
|
border-bottom: none;
|
|
336
344
|
}
|
|
@@ -28,6 +28,7 @@ export declare class GridState<T> {
|
|
|
28
28
|
private _ariaLabel?;
|
|
29
29
|
private _stickyHeader;
|
|
30
30
|
private _fullScreen;
|
|
31
|
+
private _workerSort;
|
|
31
32
|
private _filterOptions;
|
|
32
33
|
private _columnOrder;
|
|
33
34
|
private _visibleColsCache;
|
|
@@ -60,6 +61,16 @@ export declare class GridState<T> {
|
|
|
60
61
|
items: T[];
|
|
61
62
|
totalCount: number;
|
|
62
63
|
};
|
|
64
|
+
/** Whether worker sort should be used for the current data set. */
|
|
65
|
+
get useWorkerSort(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Async version of getProcessedItems that offloads sort/filter to a Web Worker.
|
|
68
|
+
* Falls back to sync when worker sort is not active.
|
|
69
|
+
*/
|
|
70
|
+
getProcessedItemsAsync(): Promise<{
|
|
71
|
+
items: T[];
|
|
72
|
+
totalCount: number;
|
|
73
|
+
}>;
|
|
63
74
|
private fetchServerData;
|
|
64
75
|
setData(data: T[]): void;
|
|
65
76
|
setPage(page: number): void;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { IVirtualScrollConfig, IVisibleRange } from '@alaarab/ogrid-core';
|
|
1
|
+
import type { IVirtualScrollConfig, IVisibleRange, IVisibleColumnRange } from '@alaarab/ogrid-core';
|
|
2
2
|
interface VirtualScrollEvents extends Record<string, unknown> {
|
|
3
3
|
rangeChanged: {
|
|
4
4
|
visibleRange: IVisibleRange;
|
|
5
5
|
};
|
|
6
|
+
columnRangeChanged: {
|
|
7
|
+
columnRange: IVisibleColumnRange | null;
|
|
8
|
+
};
|
|
6
9
|
configChanged: {
|
|
7
10
|
config: IVirtualScrollConfig;
|
|
8
11
|
};
|
|
@@ -21,6 +24,13 @@ export declare class VirtualScrollState {
|
|
|
21
24
|
private _ro;
|
|
22
25
|
private _resizeRafId;
|
|
23
26
|
private _cachedRange;
|
|
27
|
+
private _scrollLeft;
|
|
28
|
+
private _scrollLeftRafId;
|
|
29
|
+
private _containerWidth;
|
|
30
|
+
private _columnWidths;
|
|
31
|
+
private _cachedColumnRange;
|
|
32
|
+
private _roWidth;
|
|
33
|
+
private _resizeWidthRafId;
|
|
24
34
|
constructor(config?: IVirtualScrollConfig);
|
|
25
35
|
/** Whether virtual scrolling is active (enabled + meets the row threshold). */
|
|
26
36
|
get enabled(): boolean;
|
|
@@ -32,6 +42,19 @@ export declare class VirtualScrollState {
|
|
|
32
42
|
get visibleRange(): IVisibleRange;
|
|
33
43
|
/** Get the total scrollable height for all rows. */
|
|
34
44
|
get totalHeight(): number;
|
|
45
|
+
/** Whether column virtualization is active. */
|
|
46
|
+
get columnVirtualizationEnabled(): boolean;
|
|
47
|
+
/** Get the current visible column range (null when column virtualization is disabled). */
|
|
48
|
+
get columnRange(): IVisibleColumnRange | null;
|
|
49
|
+
/** Set the unpinned column widths for horizontal virtualization. */
|
|
50
|
+
setColumnWidths(widths: number[]): void;
|
|
51
|
+
/** Handle horizontal scroll events. RAF-throttled. */
|
|
52
|
+
handleHorizontalScroll(scrollLeft: number): void;
|
|
53
|
+
/** Observe a container element for width changes (column virtualization). */
|
|
54
|
+
observeContainerWidth(el: HTMLElement): void;
|
|
55
|
+
private disconnectWidthObserver;
|
|
56
|
+
/** Recompute visible column range and emit if changed. */
|
|
57
|
+
private recomputeColumnRange;
|
|
35
58
|
/** Handle scroll events from the table container. RAF-throttled. */
|
|
36
59
|
handleScroll(scrollTop: number): void;
|
|
37
60
|
/** Scroll the container to bring a specific row into view. */
|
|
@@ -46,6 +69,7 @@ export declare class VirtualScrollState {
|
|
|
46
69
|
/** Recompute visible range and emit if changed. */
|
|
47
70
|
private recompute;
|
|
48
71
|
onRangeChanged(handler: (data: VirtualScrollEvents['rangeChanged']) => void): () => void;
|
|
72
|
+
onColumnRangeChanged(handler: (data: VirtualScrollEvents['columnRangeChanged']) => void): () => void;
|
|
49
73
|
onConfigChanged(handler: (data: VirtualScrollEvents['configChanged']) => void): () => void;
|
|
50
74
|
destroy(): void;
|
|
51
75
|
}
|
|
@@ -100,6 +100,13 @@ export interface OGridOptions<T> {
|
|
|
100
100
|
onFirstDataRendered?: () => void;
|
|
101
101
|
/** Virtual scrolling configuration. */
|
|
102
102
|
virtualScroll?: IVirtualScrollConfig;
|
|
103
|
+
/**
|
|
104
|
+
* Offload sorting to a Web Worker to avoid blocking the main thread.
|
|
105
|
+
* - `true`: always use worker sort
|
|
106
|
+
* - `'auto'`: use worker sort when data.length > 5000
|
|
107
|
+
* - `false` (default): use synchronous sort
|
|
108
|
+
*/
|
|
109
|
+
workerSort?: boolean | 'auto';
|
|
103
110
|
/** Fixed row height in pixels. Overrides default row height (36px). */
|
|
104
111
|
rowHeight?: number;
|
|
105
112
|
/** Cell spacing/density preset. Controls cell padding throughout the grid. Default: 'normal'. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alaarab/ogrid-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "OGrid vanilla JS – framework-free data grid with sorting, filtering, pagination, and spreadsheet-style editing.",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@alaarab/ogrid-core": "2.
|
|
39
|
+
"@alaarab/ogrid-core": "2.2.0"
|
|
40
40
|
},
|
|
41
41
|
"sideEffects": [
|
|
42
42
|
"**/*.css"
|