@svgrid/grid 3.0.6 → 3.0.8
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/README.md +1 -1
- package/dist/SvGrid.controller.svelte.d.ts +2 -0
- package/dist/SvGrid.controller.svelte.js +46 -9
- package/dist/SvGrid.css +16 -5
- package/dist/SvGrid.helpers.js +2 -4
- package/dist/SvGrid.svelte +4 -1
- package/dist/cdn/{GridMenus-C-NtjlrJ.js → GridMenus-B8-Gbfq2.js} +2 -2
- package/dist/cdn/{GridMenus-TssHej3q.js → GridMenus-D15bhpX5.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-BlyCSp0q.js → SvGridCellEditor-9yz-SbdZ.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-ClRRA9lW.js → SvGridCellEditor-D7-ql1Hd.js} +1 -1
- package/dist/cdn/{SvGridChart-DsmwTyZF.js → SvGridChart-DMUOFxOe.js} +3 -3
- package/dist/cdn/{SvGridChart-B4EV8U6t.js → SvGridChart-N7ik0oWy.js} +3 -3
- package/dist/cdn/{SvGridChartBuilder-cHloHobN.js → SvGridChartBuilder-CB_Jnm2k.js} +3 -3
- package/dist/cdn/{SvGridChartBuilder-vOl7C0Za.js → SvGridChartBuilder-Ca7gF8ZK.js} +3 -3
- package/dist/cdn/{SvGridChartPanel-AcqF-Ll5.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-biMtSBPA.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
- package/dist/cdn/{SvGridChartView-I3rDHXAq.js → SvGridChartView-B1LFgoPP.js} +2 -2
- package/dist/cdn/{SvGridChartView-Dx_GmZuL.js → SvGridChartView-By3n7DGZ.js} +2 -2
- package/dist/cdn/{cell-formatting-DVVKOSI9.js → cell-formatting-Mnv84zoE.js} +27 -5
- package/dist/cdn/{chart-DEZxwC0i.js → chart-By7h2Guv.js} +1 -1
- package/dist/cdn/{chart-panel-messages-D12XcTZJ.js → chart-panel-messages-BC-tOgFY.js} +1 -1
- package/dist/cdn/{chart-panel-messages-DguS7gC5.js → chart-panel-messages-DUaXr-sb.js} +1 -1
- package/dist/cdn/{chart-summary-CMAXBGY-.js → chart-summary-BDtrNEiA.js} +1 -1
- package/dist/cdn/{chart-trend-BkEp61Gj.js → chart-trend-C1aOAPBZ.js} +1 -1
- package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
- package/dist/cdn/{export-format-YzdwBww2.js → export-format-y1wdyCcv.js} +93 -97
- package/dist/cdn/{src-CgoeSM8k.js → src-CleI0g9l.js} +3256 -3198
- package/dist/cdn/{src-DfwxB1TH.js → src-Dz625Fhx.js} +3184 -3126
- package/dist/cdn/svgrid.js +11 -11
- package/dist/cdn/svgrid.svelte-external.js +11 -11
- package/dist/cdn/validate-GEs4ZPSW.js +80 -0
- package/dist/cell-formatting.d.ts +9 -0
- package/dist/cell-formatting.js +34 -0
- package/dist/core.js +124 -21
- package/dist/export-format.d.ts +8 -0
- package/dist/export-format.js +23 -16
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/keyboard-handlers.js +5 -1
- package/dist/selection.js +14 -4
- package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
- package/dist/validate.js +10 -3
- package/dist/virtualization/column-virtualizer.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +3 -0
- package/dist/virtualization/virtualizer.js +14 -1
- package/package.json +1 -1
- package/src/SvGrid.controller.svelte.ts +45 -13
- package/src/SvGrid.css +16 -5
- package/src/SvGrid.helpers.ts +2 -4
- package/src/SvGrid.svelte +4 -1
- package/src/cell-formatting.ts +31 -0
- package/src/core.sort.test.ts +86 -0
- package/src/core.ts +114 -21
- package/src/export-dates.test.ts +114 -0
- package/src/export-format.ts +26 -14
- package/src/headless.reactivity.svelte.test.ts +89 -0
- package/src/headless.ts +1 -1
- package/src/index.ts +1 -1
- package/src/keyboard-handlers.ts +5 -1
- package/src/selection.test.ts +37 -0
- package/src/selection.ts +16 -6
- package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
- package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
- package/src/svgrid.behavior.test.ts +29 -0
- package/src/svgrid.data-push.svelte.test.ts +94 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +4 -2
- package/src/validate.test.ts +16 -0
- package/src/validate.ts +10 -2
- package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
- package/src/virtualization/virtualizer.test.ts +44 -0
- package/src/virtualization/virtualizer.ts +13 -1
- package/dist/cdn/validate-AZoD8BoR.js +0 -80
- /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
package/dist/core.js
CHANGED
|
@@ -130,14 +130,54 @@ export function tableFeatures(features) {
|
|
|
130
130
|
* text; set a column's type or supply your own comparator to override.
|
|
131
131
|
*/
|
|
132
132
|
export const sortFns = {
|
|
133
|
-
auto: (a, b) =>
|
|
134
|
-
number: (a, b) =>
|
|
135
|
-
date: (a, b) =>
|
|
136
|
-
const aa = new Date(a).getTime();
|
|
137
|
-
const bb = new Date(b).getTime();
|
|
138
|
-
return aa - bb;
|
|
139
|
-
},
|
|
133
|
+
auto: (a, b) => compareAutoValues(a, b),
|
|
134
|
+
number: (a, b) => compareNumericKeys(numberSortKey(a), numberSortKey(b)),
|
|
135
|
+
date: (a, b) => compareNumericKeys(dateSortKey(a), dateSortKey(b)),
|
|
140
136
|
};
|
|
137
|
+
/** Null, undefined and the empty string: the values every built-in sort puts first. */
|
|
138
|
+
function isBlankSortValue(value) {
|
|
139
|
+
return value == null || value === '';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Sort key for the `number` comparator. Blanks and anything that does not
|
|
143
|
+
* coerce to a number ('n/a', a placeholder) become -Infinity, so they sort
|
|
144
|
+
* together ahead of every number instead of producing NaN. A NaN key made the
|
|
145
|
+
* comparator inconsistent - NaN compared equal to everything while the numbers
|
|
146
|
+
* around it did not - and `Array.prototype.sort` then returned a visibly
|
|
147
|
+
* unsorted column (#104).
|
|
148
|
+
*/
|
|
149
|
+
function numberSortKey(value) {
|
|
150
|
+
if (isBlankSortValue(value))
|
|
151
|
+
return -Infinity;
|
|
152
|
+
const n = Number(value);
|
|
153
|
+
return Number.isNaN(n) ? -Infinity : n;
|
|
154
|
+
}
|
|
155
|
+
/** Sort key for the `date` comparator; blanks and invalid dates sort first. */
|
|
156
|
+
function dateSortKey(value) {
|
|
157
|
+
if (isBlankSortValue(value))
|
|
158
|
+
return -Infinity;
|
|
159
|
+
const t = new Date(value).getTime();
|
|
160
|
+
return Number.isNaN(t) ? -Infinity : t;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The `auto` comparator: blanks first, then numbers in numeric order, then
|
|
164
|
+
* everything else as collated text. Ordering by kind before value keeps it
|
|
165
|
+
* transitive on mixed columns - comparing a number to a string as text
|
|
166
|
+
* instead would give 2 < 10, 10 < '1a' and '1a' < 2, a cycle.
|
|
167
|
+
*/
|
|
168
|
+
function compareAutoValues(a, b) {
|
|
169
|
+
const aBlank = isBlankSortValue(a);
|
|
170
|
+
const bBlank = isBlankSortValue(b);
|
|
171
|
+
if (aBlank || bBlank)
|
|
172
|
+
return aBlank === bBlank ? 0 : aBlank ? -1 : 1;
|
|
173
|
+
const aNum = typeof a === 'number';
|
|
174
|
+
const bNum = typeof b === 'number';
|
|
175
|
+
if (aNum && bNum)
|
|
176
|
+
return compareNumericKeys(numberSortKey(a), numberSortKey(b));
|
|
177
|
+
if (aNum !== bNum)
|
|
178
|
+
return aNum ? -1 : 1;
|
|
179
|
+
return compareCollatedKeys(String(a), String(b));
|
|
180
|
+
}
|
|
141
181
|
/**
|
|
142
182
|
* Built-in match functions, named by {@link ColumnFilter}'s `fn`.
|
|
143
183
|
* `includesString` is case-insensitive substring; `equals` is strict identity.
|
|
@@ -736,18 +776,42 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
736
776
|
let keys = new Array(n);
|
|
737
777
|
let compare;
|
|
738
778
|
let keyOf;
|
|
779
|
+
let autoMode;
|
|
739
780
|
if (comparator === sortFns.number) {
|
|
740
|
-
keyOf = (row) =>
|
|
781
|
+
keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId));
|
|
741
782
|
for (let i = 0; i < n; i++)
|
|
742
783
|
keys[i] = keyOf(rows[i]);
|
|
743
784
|
compare = compareNumericKeys;
|
|
744
785
|
}
|
|
745
786
|
else if (comparator === sortFns.date) {
|
|
746
|
-
keyOf = (row) =>
|
|
787
|
+
keyOf = (row) => dateSortKey(row.getCellValueByColumnId(columnId));
|
|
788
|
+
for (let i = 0; i < n; i++)
|
|
789
|
+
keys[i] = keyOf(rows[i]);
|
|
790
|
+
compare = compareNumericKeys;
|
|
791
|
+
}
|
|
792
|
+
else if (comparator === sortFns.auto && (autoMode = autoSortMode(rows, columnId)) === 'number') {
|
|
793
|
+
// A column with no declared type whose values are all numbers. Sorting
|
|
794
|
+
// it as text put 10 before 2 (#104).
|
|
795
|
+
keyOf = (row) => {
|
|
796
|
+
const value = row.getCellValueByColumnId(columnId);
|
|
797
|
+
// A text value arriving in a tick changes the mode; only a full sort
|
|
798
|
+
// can place it.
|
|
799
|
+
return typeof value === 'number' || isBlankSortValue(value)
|
|
800
|
+
? numberSortKey(value)
|
|
801
|
+
: MISSING_KEY;
|
|
802
|
+
};
|
|
747
803
|
for (let i = 0; i < n; i++)
|
|
748
804
|
keys[i] = keyOf(rows[i]);
|
|
749
805
|
compare = compareNumericKeys;
|
|
750
806
|
}
|
|
807
|
+
else if (autoMode === 'mixed') {
|
|
808
|
+
// Numbers and text in one column. Rare, so no key tricks: the raw
|
|
809
|
+
// values through the pairwise comparator, which orders by kind first.
|
|
810
|
+
keyOf = (row) => row.getCellValueByColumnId(columnId);
|
|
811
|
+
for (let i = 0; i < n; i++)
|
|
812
|
+
keys[i] = keyOf(rows[i]);
|
|
813
|
+
compare = compareAutoValues;
|
|
814
|
+
}
|
|
751
815
|
else if (comparator === sortFns.auto) {
|
|
752
816
|
const strings = new Array(n);
|
|
753
817
|
// Decide whether ranking is worth attempting BEFORE paying for it.
|
|
@@ -770,7 +834,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
770
834
|
const sample = new Set();
|
|
771
835
|
let sampled = 0;
|
|
772
836
|
for (let i = 0; i < n; i += stride) {
|
|
773
|
-
sample.add(
|
|
837
|
+
sample.add(autoTextKey(rows[i].getCellValueByColumnId(columnId)));
|
|
774
838
|
sampled++;
|
|
775
839
|
}
|
|
776
840
|
// Only attempt ranking when the sample suggests real repetition.
|
|
@@ -778,7 +842,7 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
778
842
|
distinct = new Set();
|
|
779
843
|
}
|
|
780
844
|
for (let i = 0; i < n; i++) {
|
|
781
|
-
const s =
|
|
845
|
+
const s = autoTextKey(rows[i].getCellValueByColumnId(columnId));
|
|
782
846
|
strings[i] = s;
|
|
783
847
|
if (distinct) {
|
|
784
848
|
distinct.add(s);
|
|
@@ -809,12 +873,21 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
809
873
|
compare = compareNumericKeys;
|
|
810
874
|
// A replacement whose text is not among the ranked values has no
|
|
811
875
|
// rank; the repair path treats that as "sort everything again".
|
|
812
|
-
keyOf = (row) =>
|
|
876
|
+
keyOf = (row) => {
|
|
877
|
+
const value = row.getCellValueByColumnId(columnId);
|
|
878
|
+
// A number arriving in a tick makes the column mixed; resort fully.
|
|
879
|
+
if (typeof value === 'number')
|
|
880
|
+
return MISSING_KEY;
|
|
881
|
+
return rankOf.get(autoTextKey(value)) ?? MISSING_KEY;
|
|
882
|
+
};
|
|
813
883
|
}
|
|
814
884
|
else {
|
|
815
885
|
keys = strings;
|
|
816
886
|
compare = compareCollatedKeys;
|
|
817
|
-
keyOf = (row) =>
|
|
887
|
+
keyOf = (row) => {
|
|
888
|
+
const value = row.getCellValueByColumnId(columnId);
|
|
889
|
+
return typeof value === 'number' ? MISSING_KEY : autoTextKey(value);
|
|
890
|
+
};
|
|
818
891
|
}
|
|
819
892
|
}
|
|
820
893
|
else {
|
|
@@ -1037,14 +1110,39 @@ export function createSortedRowModel(localSortFns = sortFns) {
|
|
|
1037
1110
|
};
|
|
1038
1111
|
}
|
|
1039
1112
|
/**
|
|
1040
|
-
* Numeric key comparison for the built-in `number
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1113
|
+
* Numeric key comparison for the built-in `number`, `date` and numeric
|
|
1114
|
+
* `auto` comparators. Keys are never NaN (blanks and unparseable values map to
|
|
1115
|
+
* -Infinity), but two -Infinity keys subtract to NaN, so equal keys return 0
|
|
1116
|
+
* explicitly: the multi-clause sort reads any non-zero result, NaN included,
|
|
1117
|
+
* as decided and would skip the next clause.
|
|
1045
1118
|
*/
|
|
1046
1119
|
function compareNumericKeys(a, b) {
|
|
1047
|
-
return a - b;
|
|
1120
|
+
return a === b ? 0 : a - b;
|
|
1121
|
+
}
|
|
1122
|
+
/** Text key for the `auto` comparator: blanks become '' so they collate first. */
|
|
1123
|
+
function autoTextKey(value) {
|
|
1124
|
+
return isBlankSortValue(value) ? '' : String(value);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* How the `auto` comparator should treat a column: 'number' when every
|
|
1128
|
+
* non-blank value is a number, 'mixed' when numbers share it with anything
|
|
1129
|
+
* else, 'text' otherwise. One pass, stopping as soon as the answer is known.
|
|
1130
|
+
*/
|
|
1131
|
+
function autoSortMode(rows, columnId) {
|
|
1132
|
+
let numbers = false;
|
|
1133
|
+
let other = false;
|
|
1134
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1135
|
+
const value = rows[i].getCellValueByColumnId(columnId);
|
|
1136
|
+
if (isBlankSortValue(value))
|
|
1137
|
+
continue;
|
|
1138
|
+
if (typeof value === 'number')
|
|
1139
|
+
numbers = true;
|
|
1140
|
+
else
|
|
1141
|
+
other = true;
|
|
1142
|
+
if (numbers && other)
|
|
1143
|
+
return 'mixed';
|
|
1144
|
+
}
|
|
1145
|
+
return numbers ? 'number' : 'text';
|
|
1048
1146
|
}
|
|
1049
1147
|
/**
|
|
1050
1148
|
* Text key comparison for the built-in `auto` comparator.
|
|
@@ -1321,8 +1419,13 @@ export function createSvGridCore(options) {
|
|
|
1321
1419
|
const original = data[index];
|
|
1322
1420
|
const kept = previous?.[index];
|
|
1323
1421
|
if (kept && kept.original === original) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1422
|
+
// Read before writing: a row nobody rendered (a million off-screen
|
|
1423
|
+
// placeholders under a row model) has nothing memoised, and a
|
|
1424
|
+
// store per row was a measurable share of each block landing.
|
|
1425
|
+
if (kept[ROW_VALUES] !== null)
|
|
1426
|
+
kept[ROW_VALUES] = null;
|
|
1427
|
+
if (kept[ROW_CELLS] !== null)
|
|
1428
|
+
kept[ROW_CELLS] = null;
|
|
1326
1429
|
cachedBaseRows[index] = kept;
|
|
1327
1430
|
continue;
|
|
1328
1431
|
}
|
package/dist/export-format.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ export { computeColumnStat, contrastText, formatsNeedingStats, resolveCellFormat
|
|
|
21
21
|
/** Best-effort coercion of a cell value to a Date. Accepts Date, epoch ms
|
|
22
22
|
* numbers, and parseable date strings. Returns null when it isn't a date. */
|
|
23
23
|
export declare function coerceExportDate(value: unknown): Date | null;
|
|
24
|
+
/**
|
|
25
|
+
* A Date as ISO 8601 in LOCAL time with its offset, e.g.
|
|
26
|
+
* `2024-01-15T00:00:00.000+02:00`. Text exports used `toISOString()`, which
|
|
27
|
+
* is UTC: local midnight on the 15th came out as `2024-01-14T22:00:00.000Z`
|
|
28
|
+
* east of UTC, and anything reading the date part got the wrong day (#103).
|
|
29
|
+
* Same instant, but the date part is the one the grid showed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toLocalIsoString(d: Date): string;
|
|
24
32
|
/**
|
|
25
33
|
* Format a raw cell value to its on-screen display string using the column's
|
|
26
34
|
* `format` config. Mirrors the grid's own numeric / currency / percent / date
|
package/dist/export-format.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatNumericWithConfig, getDateFormatter, resolveDatePattern, } from './cell-formatting.js';
|
|
1
|
+
import { formatNumericWithConfig, getDateFormatter, parseDateValue, resolveDatePattern, } from './cell-formatting.js';
|
|
2
2
|
// Re-export the pure conditional-formatting engine here too, so the
|
|
3
3
|
// Svelte-free `@svgrid/grid/format` subpath carries it for @svgrid/enterprise's
|
|
4
4
|
// export code (which must not pull in SvGrid.svelte).
|
|
@@ -6,17 +6,24 @@ export { computeColumnStat, contrastText, formatsNeedingStats, resolveCellFormat
|
|
|
6
6
|
/** Best-effort coercion of a cell value to a Date. Accepts Date, epoch ms
|
|
7
7
|
* numbers, and parseable date strings. Returns null when it isn't a date. */
|
|
8
8
|
export function coerceExportDate(value) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
return parseDateValue(value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A Date as ISO 8601 in LOCAL time with its offset, e.g.
|
|
13
|
+
* `2024-01-15T00:00:00.000+02:00`. Text exports used `toISOString()`, which
|
|
14
|
+
* is UTC: local midnight on the 15th came out as `2024-01-14T22:00:00.000Z`
|
|
15
|
+
* east of UTC, and anything reading the date part got the wrong day (#103).
|
|
16
|
+
* Same instant, but the date part is the one the grid showed.
|
|
17
|
+
*/
|
|
18
|
+
export function toLocalIsoString(d) {
|
|
19
|
+
const pad = (n, w = 2) => String(Math.abs(n)).padStart(w, '0');
|
|
20
|
+
const offset = -d.getTimezoneOffset();
|
|
21
|
+
const sign = offset < 0 ? '-' : '+';
|
|
22
|
+
const year = d.getFullYear();
|
|
23
|
+
return ((year < 0 ? '-' + pad(year, 6) : pad(year, 4)) +
|
|
24
|
+
`-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
|
25
|
+
`T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}` +
|
|
26
|
+
`${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`);
|
|
20
27
|
}
|
|
21
28
|
/**
|
|
22
29
|
* Format a raw cell value to its on-screen display string using the column's
|
|
@@ -190,7 +197,7 @@ function csvCell(value, delimiter) {
|
|
|
190
197
|
const s = value == null
|
|
191
198
|
? ''
|
|
192
199
|
: value instanceof Date
|
|
193
|
-
? value
|
|
200
|
+
? toLocalIsoString(value)
|
|
194
201
|
: String(value);
|
|
195
202
|
if (s === '')
|
|
196
203
|
return s;
|
|
@@ -231,7 +238,7 @@ const HTML_ESCAPE = {
|
|
|
231
238
|
"'": ''',
|
|
232
239
|
};
|
|
233
240
|
function htmlCell(value) {
|
|
234
|
-
const s = value == null ? '' : value instanceof Date ? value
|
|
241
|
+
const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
|
|
235
242
|
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPE[c]);
|
|
236
243
|
}
|
|
237
244
|
/**
|
|
@@ -311,7 +318,7 @@ export async function serializeJson(rows, fields, opts = {}) {
|
|
|
311
318
|
}
|
|
312
319
|
/** Escape a Markdown table cell: pipes and newlines would break the row. */
|
|
313
320
|
function mdCell(value) {
|
|
314
|
-
const s = value == null ? '' : value instanceof Date ? value
|
|
321
|
+
const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
|
|
315
322
|
return s.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
|
|
316
323
|
}
|
|
317
324
|
/**
|
|
@@ -351,7 +358,7 @@ const XML_ESCAPE = {
|
|
|
351
358
|
"'": ''',
|
|
352
359
|
};
|
|
353
360
|
function xmlText(value) {
|
|
354
|
-
const s = value == null ? '' : value instanceof Date ? value
|
|
361
|
+
const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
|
|
355
362
|
// Stripping control characters is the point: XML 1.0 forbids them outright,
|
|
356
363
|
// so a stray \x00 from a data source would produce an unopenable file.
|
|
357
364
|
// eslint-disable-next-line no-control-regex
|
package/dist/headless.d.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, createSvGridCore, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type ColumnFilter, type ColumnFiltersState, type ExpandedState, type GridColumnDef, type GridColumns, type GroupAggregator, type GroupingState, type Header, type HeaderContext, type HeaderGroup, type PaginationState, type Row, type RowData, type RowModel, type RowModelFactory, type RowSelectionState, type SortingState, type Store, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core.js';
|
|
27
27
|
export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
|
|
28
28
|
export { createGridState, createSvGridState } from './createGridState.svelte.js';
|
|
29
|
-
export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
|
|
29
|
+
export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
|
|
30
30
|
export { createVirtualizer } from './virtualization/virtualizer.js';
|
|
31
31
|
export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
|
|
32
32
|
export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
|
package/dist/headless.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, createSvGridCore, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core.js';
|
|
27
27
|
export { createGrid, createSvGrid } from './createGrid.svelte.js';
|
|
28
28
|
export { createGridState, createSvGridState } from './createGridState.svelte.js';
|
|
29
|
-
export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
|
|
29
|
+
export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
|
|
30
30
|
// The virtualizers are pure window arithmetic (scroll offset in, visible
|
|
31
31
|
// range out) with no DOM of their own, and a custom renderer over 50k rows
|
|
32
32
|
// needs one. They were only on the barrel, so the Headless demos imported
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt,
|
|
|
3
3
|
export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, type Cell, type CellContext, type CellSpanParams, type ValueParserParams, type EditorContext, type CellData, type ActiveCellState, type CellFormatConfig, type CellFormatter, type Column, type ColumnDef, type ColumnDefTemplate, type GridColumnDef, type GridColumns, type GroupAggregator, type Header, type HeaderContext, type HeaderGroup, type Row, type RowData, type SortingState, type SvGrid as SvGridInstance, type SvGridOptions, type TableFeatures, type TreeRowModelOptions, type FlattenTreeOptions, type Updater, } from './core.js';
|
|
4
4
|
export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
|
|
5
5
|
export { createGridState, createSvGridState } from './createGridState.svelte.js';
|
|
6
|
-
export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
|
|
6
|
+
export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
|
|
7
7
|
export { default as SvGrid } from './SvGrid.svelte';
|
|
8
8
|
export type { BoardConfig, BoardLane, BoardCardMoveEvent, BoardCardCommitEvent, BoardLayout, BoardSubtask, BoardComment, BoardMenuContext, BoardLaneMenuContext, BoardDrawerConfig } from './SvGrid.types.js';
|
|
9
9
|
export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte.js';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { coerceExportDate, formatValueForExport, projectGridRows, toExcelNumFmt,
|
|
|
3
3
|
export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, createSortedRowModel, createTreeRowModel, flattenTreeData, applyGroupAggregate, filterFns, rowExpandingFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, } from './core.js';
|
|
4
4
|
export { createGrid, createSvGrid } from './createGrid.svelte.js';
|
|
5
5
|
export { createGridState, createSvGridState } from './createGridState.svelte.js';
|
|
6
|
-
export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
|
|
6
|
+
export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte.js';
|
|
7
7
|
export { default as SvGrid } from './SvGrid.svelte';
|
|
8
8
|
export { registerBoardView, getBoardView, hasBoardView } from './board-view.svelte.js';
|
|
9
9
|
// Built-in AI helpers (free): natural-language filter, smart-fill, summarize,
|
|
@@ -189,7 +189,11 @@ export function createKeyboard(ctx) {
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
if (column?.columnDef.editorType === "checkbox") {
|
|
192
|
-
|
|
192
|
+
// Same rule as a click: `editable: false` (or an editable callback
|
|
193
|
+
// saying no) keeps the checkbox read-only.
|
|
194
|
+
if (ctx.isCellEditableAt?.(current.rowIndex, current.colIndex) ?? true) {
|
|
195
|
+
ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
|
|
196
|
+
}
|
|
193
197
|
return;
|
|
194
198
|
}
|
|
195
199
|
ctx.onCellDoubleClick(current.rowIndex, current.colIndex);
|
package/dist/selection.js
CHANGED
|
@@ -622,10 +622,20 @@ export function createSelection(ctx) {
|
|
|
622
622
|
columnId: column.id,
|
|
623
623
|
row: row.original,
|
|
624
624
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
625
|
+
// A declared checkbox editor toggles on click unless the cell is not
|
|
626
|
+
// editable. A value that only HAPPENS to be boolean is drawn as a read-only
|
|
627
|
+
// checkbox, so it toggles only when inline editing is on - it used to flip
|
|
628
|
+
// and write the row on any click, in grids with editing switched off. A
|
|
629
|
+
// column with its own `cell` renderer draws (and handles) the value itself.
|
|
630
|
+
const declaredCheckbox = column.columnDef.editorType === "checkbox";
|
|
631
|
+
const inferredCheckbox = !declaredCheckbox &&
|
|
632
|
+
typeof cellValue === "boolean" &&
|
|
633
|
+
column.columnDef.cell == null;
|
|
634
|
+
if (declaredCheckbox || inferredCheckbox) {
|
|
635
|
+
const editable = ctx.isCellEditableAt?.(rowIndex, colIndex) ?? true;
|
|
636
|
+
if (editable && (declaredCheckbox || ctx.editingEnabled)) {
|
|
637
|
+
ctx.toggleBooleanCell(rowIndex, colIndex);
|
|
638
|
+
}
|
|
629
639
|
setActiveCell(rowIndex, colIndex);
|
|
630
640
|
setSelection(rowIndex, colIndex);
|
|
631
641
|
ctx.editingCell = null;
|
|
@@ -16,7 +16,11 @@ const shallowCompare = (a, b) => {
|
|
|
16
16
|
* comparison. Call from `<script>` so the selector is contextually typed.
|
|
17
17
|
*/
|
|
18
18
|
export function subscribeGrid(grid, selector) {
|
|
19
|
-
|
|
19
|
+
// $state, not a plain variable: core.ts keeps a deliberately framework-free
|
|
20
|
+
// store, so this Svelte adapter is where reactivity belongs. Without it a
|
|
21
|
+
// `$derived` reading .current never re-runs and the docs' headless examples
|
|
22
|
+
// render their seeded state forever.
|
|
23
|
+
let current = $state(selector(grid.store.state));
|
|
20
24
|
grid.store.subscribe(() => {
|
|
21
25
|
const next = selector(grid.store.state);
|
|
22
26
|
if (!shallowCompare(current, next))
|
package/dist/validate.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { rowPlaceholderState } from './server-block-cache.js';
|
|
2
2
|
/** How many rows to sample when deciding whether a `field` exists. */
|
|
3
3
|
const FIELD_SAMPLE_ROWS = 10;
|
|
4
|
+
/** How many rows to look through for that sample, placeholders included. */
|
|
5
|
+
const FIELD_SCAN_ROWS = 1000;
|
|
4
6
|
const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/';
|
|
5
7
|
/** Every leaf column, flattened through group columns. */
|
|
6
8
|
function leaves(columns, out = []) {
|
|
@@ -23,10 +25,15 @@ export function validateGridConfig(input) {
|
|
|
23
25
|
// no data keys at all and say nothing about the columns.
|
|
24
26
|
// Stops at the sample size: this runs on every data change, and a filter
|
|
25
27
|
// over all the rows first was O(n) per tick on a live feed.
|
|
28
|
+
// The scan is bounded too: a row model scrolled to the middle of a million
|
|
29
|
+
// rows, or just purged by a sort, hands over a list that is placeholders
|
|
30
|
+
// up to the loaded block, and walking all of them for ten real rows was
|
|
31
|
+
// O(n) per block landing. Finding none means nothing to judge by.
|
|
26
32
|
const sample = [];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
const data = input.data ?? [];
|
|
34
|
+
const scan = Math.min(data.length, FIELD_SCAN_ROWS);
|
|
35
|
+
for (let i = 0; i < scan && sample.length < FIELD_SAMPLE_ROWS; i += 1) {
|
|
36
|
+
const row = data[i];
|
|
30
37
|
if (!rowPlaceholderState(row))
|
|
31
38
|
sample.push(row);
|
|
32
39
|
}
|
|
@@ -10,6 +10,7 @@ export declare function createColumnVirtualizer(input: {
|
|
|
10
10
|
setViewportWidth(viewportWidth: number): void;
|
|
11
11
|
setHorizontalOffset(scrollOffset: number): void;
|
|
12
12
|
setOptions(next: Partial<import("./types.js").VirtualizerOptions>): void;
|
|
13
|
+
measure(): void;
|
|
13
14
|
setScrollOffset(scrollOffset: number): void;
|
|
14
15
|
setViewportHeight(viewportHeight: number): void;
|
|
15
16
|
scrollToIndex(index: number): void;
|
|
@@ -2,6 +2,7 @@ import type { VirtualizerOptions } from './types.js';
|
|
|
2
2
|
export declare function createSvelteVirtualizer(options: VirtualizerOptions): {
|
|
3
3
|
readonly version: number;
|
|
4
4
|
setOptions: (next: Partial<VirtualizerOptions>) => void;
|
|
5
|
+
measure: () => void;
|
|
5
6
|
setScrollOffset: (scrollOffset: number) => void;
|
|
6
7
|
setViewportHeight: (viewportHeight: number) => void;
|
|
7
8
|
scrollToIndex: (index: number) => void;
|
|
@@ -10,6 +10,7 @@ export function createSvelteVirtualizer(options) {
|
|
|
10
10
|
return version;
|
|
11
11
|
},
|
|
12
12
|
setOptions: virtualizer.setOptions,
|
|
13
|
+
measure: virtualizer.measure,
|
|
13
14
|
setScrollOffset: virtualizer.setScrollOffset,
|
|
14
15
|
setViewportHeight: virtualizer.setViewportHeight,
|
|
15
16
|
scrollToIndex: virtualizer.scrollToIndex,
|
|
@@ -16,6 +16,9 @@ type VirtualizerListener = () => void;
|
|
|
16
16
|
export declare function buildPreMeasureItems(count: number, estimateSize: number, viewportHeight: number, overscan: number): Array<VirtualItem>;
|
|
17
17
|
export declare function createVirtualizer(initial: VirtualizerOptions): {
|
|
18
18
|
setOptions(next: Partial<VirtualizerOptions>): void;
|
|
19
|
+
/** Drop the cached offsets and re-read every size from `estimateSize`.
|
|
20
|
+
* Call after the sizes a stable size function reports have changed. */
|
|
21
|
+
measure(): void;
|
|
19
22
|
setScrollOffset(scrollOffset: number): void;
|
|
20
23
|
setViewportHeight(viewportHeight: number): void;
|
|
21
24
|
scrollToIndex(index: number): void;
|
|
@@ -210,9 +210,22 @@ export function createVirtualizer(initial) {
|
|
|
210
210
|
return {
|
|
211
211
|
setOptions(next) {
|
|
212
212
|
const merged = { ...options, ...next };
|
|
213
|
-
|
|
213
|
+
// Passing a size function means "sizes may have changed", even when it is
|
|
214
|
+
// the same reference: the documented variable-height pattern is a stable
|
|
215
|
+
// function reading a mutable map, and keying the cache on identity alone
|
|
216
|
+
// left the offsets and total stale after a row grew (#102).
|
|
217
|
+
const remeasure = typeof next.estimateSize === 'function';
|
|
218
|
+
if (!remeasure && sameOptions(options, merged))
|
|
214
219
|
return;
|
|
215
220
|
options = merged;
|
|
221
|
+
if (remeasure)
|
|
222
|
+
offsetCache = null;
|
|
223
|
+
recalc();
|
|
224
|
+
},
|
|
225
|
+
/** Drop the cached offsets and re-read every size from `estimateSize`.
|
|
226
|
+
* Call after the sizes a stable size function reports have changed. */
|
|
227
|
+
measure() {
|
|
228
|
+
offsetCache = null;
|
|
216
229
|
recalc();
|
|
217
230
|
},
|
|
218
231
|
setScrollOffset(scrollOffset) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
4
4
|
"description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
|
|
5
5
|
"author": "jQWidgets <boikom@jqwidgets.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -845,7 +845,18 @@ export function createSvGridController<
|
|
|
845
845
|
// `data` is optional on the type (a `rowModel` may supply the rows
|
|
846
846
|
// instead), so every read falls back to empty rather than to undefined.
|
|
847
847
|
const EMPTY_DATA: ReadonlyArray<TData> = [];
|
|
848
|
-
|
|
848
|
+
// Seeded as a copy, for the same reason the sync effect below makes one: the
|
|
849
|
+
// row model tells data apart by array identity, so the grid has to own an
|
|
850
|
+
// array the consumer cannot grow behind its back.
|
|
851
|
+
let internalData = $state.raw<ReadonlyArray<TData>>(
|
|
852
|
+
props.data?.length ? props.data.slice() : EMPTY_DATA,
|
|
853
|
+
);
|
|
854
|
+
// What the last sync saw, so the effect can tell a replacement (drop the
|
|
855
|
+
// cell edits) from a push or splice (keep them) and skip a no-op run.
|
|
856
|
+
// svelte-ignore state_referenced_locally
|
|
857
|
+
let lastData: ReadonlyArray<TData> = props.data ?? EMPTY_DATA;
|
|
858
|
+
// svelte-ignore state_referenced_locally
|
|
859
|
+
let lastDataLength = props.data?.length ?? 0;
|
|
849
860
|
// Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
|
|
850
861
|
// format defaults once, up front, so every downstream reader sees a normal
|
|
851
862
|
// column. Explicit fields on the ColumnDef always win.
|
|
@@ -893,12 +904,27 @@ export function createSvGridController<
|
|
|
893
904
|
}
|
|
894
905
|
|
|
895
906
|
$effect(() => {
|
|
907
|
+
const next = props.data ?? EMPTY_DATA;
|
|
908
|
+
// Reading `length` is what subscribes this effect to a `$state` array's own
|
|
909
|
+
// length signal, so `rows.push(...)` and `rows.splice(...)` re-run it.
|
|
910
|
+
// Without this read nothing in the grid tracks the row count at all:
|
|
911
|
+
// `getRowModel` reads `.length` only inside its cache miss, and the miss is
|
|
912
|
+
// keyed on the array reference, which a mutation does not change.
|
|
913
|
+
const length = next.length;
|
|
914
|
+
const replaced = next !== lastData;
|
|
915
|
+
if (!replaced && length === lastDataLength) return;
|
|
916
|
+
lastData = next;
|
|
917
|
+
lastDataLength = length;
|
|
918
|
+
// A fresh array object, because `getRowModel` memoises on identity. The
|
|
919
|
+
// elements are the same objects, so a row that did not move keeps its row
|
|
920
|
+
// object and a field write still reaches its cell through the proxy.
|
|
921
|
+
internalData = length === 0 ? EMPTY_DATA : next.slice();
|
|
896
922
|
// When the consumer replaces `data` (e.g. a "Reset" button), drop any
|
|
897
923
|
// accumulated cell-edit overrides - otherwise `getCellDisplayValue`
|
|
898
924
|
// would keep returning the old edited values from `editedCellValues`
|
|
899
|
-
// even though the underlying data has been replaced.
|
|
900
|
-
|
|
901
|
-
editedCellValues = {};
|
|
925
|
+
// even though the underlying data has been replaced. Growing or shrinking
|
|
926
|
+
// the array is not a reset, so it keeps them.
|
|
927
|
+
if (replaced) editedCellValues = {};
|
|
902
928
|
});
|
|
903
929
|
$effect(() => {
|
|
904
930
|
internalColumns = resolveCols(props.columns);
|
|
@@ -1692,15 +1718,21 @@ export function createSvGridController<
|
|
|
1692
1718
|
|
|
1693
1719
|
let rows = rawRows;
|
|
1694
1720
|
if (globalFilter.trim()) {
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
);
|
|
1721
|
+
const locale = props.filterLocale ?? props.localization?.locale;
|
|
1722
|
+
const needle = normalizeForFilter(globalFilter, locale);
|
|
1723
|
+
// Read the values, not the cells. `getAllCells()` built a cell object
|
|
1724
|
+
// with two closures per column on EVERY row and kept them on the row:
|
|
1725
|
+
// ten million objects for one keystroke over a million rows, and a
|
|
1726
|
+
// search that froze the page for seconds. `cell.getValue()` and
|
|
1727
|
+
// `getCellValueByColumnId` read the same per-row values array, so the
|
|
1728
|
+
// answer is the same. The columns are the ones the rows were built with.
|
|
1729
|
+
const columnIds = grid.getAllColumns().map((c) => c.id);
|
|
1730
|
+
rows = rows.filter((row) => {
|
|
1731
|
+
for (const id of columnIds) {
|
|
1732
|
+
if (normalizeForFilter(String(row.getCellValueByColumnId(id) ?? ""), locale).includes(needle)) return true;
|
|
1733
|
+
}
|
|
1734
|
+
return false;
|
|
1735
|
+
});
|
|
1704
1736
|
}
|
|
1705
1737
|
|
|
1706
1738
|
rows = applyColumnFilters(rows);
|
package/src/SvGrid.css
CHANGED
|
@@ -1387,9 +1387,6 @@ select.sv-grid-fr-editor {
|
|
|
1387
1387
|
.sv-grid-cell-active,
|
|
1388
1388
|
.sv-grid-cell-editing {
|
|
1389
1389
|
position: relative;
|
|
1390
|
-
/* High enough to sit above pinned-column stacking contexts (z=4) so the
|
|
1391
|
-
list/chips popover overlays adjacent cells instead of being clipped. */
|
|
1392
|
-
z-index: 20;
|
|
1393
1390
|
/* `box-shadow: inset` is bound to the cell's own box, so the ring
|
|
1394
1391
|
clips cleanly with the cell when it's partially scrolled out of
|
|
1395
1392
|
view (and shows up consistently at the right edge of a virtualized
|
|
@@ -1399,10 +1396,24 @@ select.sv-grid-fr-editor {
|
|
|
1399
1396
|
inconsistent at scroll boundaries. */
|
|
1400
1397
|
box-shadow: inset 0 0 0 2px var(--sg-accent, #0b63f3);
|
|
1401
1398
|
outline: none;
|
|
1402
|
-
|
|
1403
|
-
|
|
1399
|
+
}
|
|
1400
|
+
/* Only the EDITING cell escapes its own box and outranks the pinned-column
|
|
1401
|
+
stacking contexts (z=4), so its list/chips popout overlays the neighbours
|
|
1402
|
+
instead of being clipped. The merely-ACTIVE cell used to get the same
|
|
1403
|
+
treatment, and on a grid scrolled sideways that made it paint straight over
|
|
1404
|
+
the sticky selection / row-number column: the checkbox disappeared and the
|
|
1405
|
+
cell's text - unclipped, because overflow was visible - spilled across it.
|
|
1406
|
+
A grid seeds its active cell at (0,0), so this hit the FIRST row of any
|
|
1407
|
+
horizontally scrolled grid with `showRowSelection`, untouched by the user. */
|
|
1408
|
+
.sv-grid-cell-editing {
|
|
1409
|
+
z-index: 20;
|
|
1404
1410
|
overflow: visible;
|
|
1405
1411
|
}
|
|
1412
|
+
/* Above ordinary cells so the ring is not overdrawn by a neighbour, below the
|
|
1413
|
+
sticky columns (z=4) so those keep covering it as they should. */
|
|
1414
|
+
.sv-grid-cell-active:not(.sv-grid-cell-editing) {
|
|
1415
|
+
z-index: 3;
|
|
1416
|
+
}
|
|
1406
1417
|
/* The active cell a grid mounts with is seeded at (0,0), not chosen: no
|
|
1407
1418
|
ring until a click, an arrow key or the api activates a cell (the same
|
|
1408
1419
|
gate the fill handle has always had). Once activated it stays through a
|