@genesislcap/grid-pro 15.21.1 → 15.22.1
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/.cursor/rules/ag-embedded-components.mdc +83 -0
- package/dist/custom-elements.json +794 -0
- package/dist/dts/column-filters/enum-column.filter.d.ts +204 -0
- package/dist/dts/column-filters/enum-column.filter.d.ts.map +1 -0
- package/dist/dts/column-filters/enum-column.floating-filter.d.ts +23 -0
- package/dist/dts/column-filters/enum-column.floating-filter.d.ts.map +1 -0
- package/dist/dts/column-filters/index.d.ts +3 -0
- package/dist/dts/column-filters/index.d.ts.map +1 -0
- package/dist/dts/datasource/filter.utils.d.ts.map +1 -1
- package/dist/dts/grid-pro-beta.d.ts +16 -21
- package/dist/dts/grid-pro-beta.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/column-filters/enum-column.filter.js +525 -0
- package/dist/esm/column-filters/enum-column.floating-filter.js +109 -0
- package/dist/esm/column-filters/index.js +2 -0
- package/dist/esm/datasource/filter.utils.js +29 -6
- package/dist/esm/grid-pro-beta.js +68 -29
- package/dist/esm/index.js +1 -0
- package/dist/grid-pro.api.json +1577 -105
- package/dist/grid-pro.d.ts +253 -21
- package/package.json +13 -13
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { css } from '@microsoft/fast-element';
|
|
2
|
+
import { capitalCase } from 'change-case';
|
|
3
|
+
import { logger } from '../utils';
|
|
4
|
+
/**
|
|
5
|
+
* The available Grid Pro column filter component names.
|
|
6
|
+
* @remarks Register-able via the grid `components` map; `enumFilter` is usable as a
|
|
7
|
+
* `ColDef.filter` value and `enumFloatingFilter` as a `ColDef.floatingFilterComponent`.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export var GridProFilterTypes;
|
|
11
|
+
(function (GridProFilterTypes) {
|
|
12
|
+
GridProFilterTypes["enumFilter"] = "enumColumnFilter";
|
|
13
|
+
GridProFilterTypes["enumFloatingFilter"] = "enumColumnFloatingFilter";
|
|
14
|
+
})(GridProFilterTypes || (GridProFilterTypes = {}));
|
|
15
|
+
const BLANKS_LABEL = '(Blanks)';
|
|
16
|
+
/**
|
|
17
|
+
* Builds the value-to-label formatter for an enum filter, honouring `valueFormatter` from the
|
|
18
|
+
* filter params and labelling `null` (blanks) consistently.
|
|
19
|
+
* @remarks Shared by {@link EnumColumnFilter} and the floating filter so both render the same
|
|
20
|
+
* labels from `ColDef.filterParams` alone.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export function createEnumValueLabelFormatter(params) {
|
|
24
|
+
const userFormatter = params === null || params === void 0 ? void 0 : params.valueFormatter;
|
|
25
|
+
const format = userFormatter
|
|
26
|
+
? (value) => String(userFormatter({
|
|
27
|
+
value,
|
|
28
|
+
api: params === null || params === void 0 ? void 0 : params.api,
|
|
29
|
+
colDef: params === null || params === void 0 ? void 0 : params.colDef,
|
|
30
|
+
column: params === null || params === void 0 ? void 0 : params.column,
|
|
31
|
+
context: params === null || params === void 0 ? void 0 : params.context,
|
|
32
|
+
}))
|
|
33
|
+
: (value) => capitalCase(value);
|
|
34
|
+
return (value) => (value === null ? BLANKS_LABEL : format(value));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders an enum filter model as a short summary, e.g. `(2) Active, Pending`.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export function formatEnumFilterModelAsString(model, formatLabel) {
|
|
41
|
+
if (!(model === null || model === void 0 ? void 0 : model.values)) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
if (model.values.length === 0) {
|
|
45
|
+
return '(None)';
|
|
46
|
+
}
|
|
47
|
+
return `(${model.values.length}) ${model.values.map(formatLabel).join(', ')}`;
|
|
48
|
+
}
|
|
49
|
+
const CSS_PREFIX = 'gp-enum-filter';
|
|
50
|
+
/**
|
|
51
|
+
* Applied to each instance's shadow root; FAST shares one constructable stylesheet between them.
|
|
52
|
+
*/
|
|
53
|
+
const filterStyles = css `
|
|
54
|
+
:host {
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: 4px;
|
|
58
|
+
padding: 6px;
|
|
59
|
+
min-width: 180px;
|
|
60
|
+
color: var(--ag-foreground-color, inherit);
|
|
61
|
+
background: var(--ag-control-panel-background-color, transparent);
|
|
62
|
+
font: inherit;
|
|
63
|
+
}
|
|
64
|
+
.${CSS_PREFIX}__search {
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
width: 100%;
|
|
67
|
+
padding: 4px 6px;
|
|
68
|
+
font: inherit;
|
|
69
|
+
color: inherit;
|
|
70
|
+
background: var(--ag-background-color, transparent);
|
|
71
|
+
border: 1px solid var(--ag-input-border-color, var(--ag-border-color, #babfc7));
|
|
72
|
+
border-radius: 3px;
|
|
73
|
+
outline: none;
|
|
74
|
+
}
|
|
75
|
+
.${CSS_PREFIX}__search:focus {
|
|
76
|
+
border-color: var(
|
|
77
|
+
--ag-input-focus-border-color,
|
|
78
|
+
var(--ag-range-selection-border-color, #2196f3)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
.${CSS_PREFIX}__select-all {
|
|
82
|
+
border-bottom: 1px solid var(--ag-border-color, #babfc7);
|
|
83
|
+
padding-bottom: 4px;
|
|
84
|
+
}
|
|
85
|
+
.${CSS_PREFIX}__list {
|
|
86
|
+
max-height: 210px;
|
|
87
|
+
overflow-y: auto;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
}
|
|
91
|
+
.${CSS_PREFIX}__item {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 6px;
|
|
95
|
+
padding: 3px 4px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
border-radius: 3px;
|
|
98
|
+
user-select: none;
|
|
99
|
+
}
|
|
100
|
+
.${CSS_PREFIX}__item:hover, .${CSS_PREFIX}__item:focus-within {
|
|
101
|
+
background: var(--ag-row-hover-color, rgba(33, 150, 243, 0.1));
|
|
102
|
+
}
|
|
103
|
+
.${CSS_PREFIX}__item input {
|
|
104
|
+
margin: 0;
|
|
105
|
+
accent-color: var(--ag-checkbox-checked-color, var(--ag-range-selection-border-color, #2196f3));
|
|
106
|
+
}
|
|
107
|
+
.${CSS_PREFIX}__item-label {
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
text-overflow: ellipsis;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
}
|
|
112
|
+
.${CSS_PREFIX}__no-matches {
|
|
113
|
+
padding: 4px;
|
|
114
|
+
opacity: 0.7;
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
/**
|
|
118
|
+
* An Excel AutoFilter-inspired set filter for enum columns, built on AG Grid Community only.
|
|
119
|
+
* @remarks
|
|
120
|
+
* Renders a searchable checkbox list of the column's enum options (from `filterParams.values`,
|
|
121
|
+
* fed by the resource field metadata or a values getter — the option list is defined by the enum,
|
|
122
|
+
* never scanned from row data) with a tri-state "(Select All)" toggle scoped to the search
|
|
123
|
+
* results, a `(Blanks)` entry shown by default, human-readable labels, and arrow-key navigation —
|
|
124
|
+
* applying on every change. Emits the same `{ filterType: 'set', values }` model as the
|
|
125
|
+
* enterprise `agSetColumnFilter`, so it is a drop-in replacement for filter-model persistence and
|
|
126
|
+
* the Genesis server-side criteria building — without requiring the enterprise `SetFilterModule`.
|
|
127
|
+
* @public
|
|
128
|
+
*/
|
|
129
|
+
export class EnumColumnFilter {
|
|
130
|
+
constructor() {
|
|
131
|
+
/** Rendered value checkboxes keyed by value; `null` keys the blanks entry. */
|
|
132
|
+
this.itemInputs = new Map();
|
|
133
|
+
/** All selectable values, in the order the metadata/getter provided them. */
|
|
134
|
+
this.allValues = [];
|
|
135
|
+
/** Whether the `(Blanks)` entry is offered — on unless `filterParams.blanks` is `false`. */
|
|
136
|
+
this.includeBlanks = true;
|
|
137
|
+
/** Guards async values resolution against out-of-date responses and use after destroy. */
|
|
138
|
+
this.valuesRequestId = 0;
|
|
139
|
+
this.loading = false;
|
|
140
|
+
/** Whether this instance installed the header tooltip getter (so destroy removes only its own). */
|
|
141
|
+
this.ownsHeaderTooltip = false;
|
|
142
|
+
/**
|
|
143
|
+
* `null` while inactive (everything passes); otherwise the selected values, with `null`
|
|
144
|
+
* standing for blanks. May contain values absent from `allValues` (e.g. a restored model
|
|
145
|
+
* referencing rows not loaded yet) — those are preserved and rendered as extra list entries
|
|
146
|
+
* so restoring a persisted model never silently narrows it, while still letting the user
|
|
147
|
+
* deselect them.
|
|
148
|
+
*/
|
|
149
|
+
this.model = null;
|
|
150
|
+
this.searchTerm = '';
|
|
151
|
+
}
|
|
152
|
+
init(params) {
|
|
153
|
+
this.params = params;
|
|
154
|
+
this.formatLabel = createEnumValueLabelFormatter(params);
|
|
155
|
+
this.installHeaderTooltip();
|
|
156
|
+
this.createGui();
|
|
157
|
+
this.resolveState();
|
|
158
|
+
}
|
|
159
|
+
getGui() {
|
|
160
|
+
return this.eGui;
|
|
161
|
+
}
|
|
162
|
+
isFilterActive() {
|
|
163
|
+
return this.model !== null;
|
|
164
|
+
}
|
|
165
|
+
doesFilterPass(params) {
|
|
166
|
+
if (this.model === null) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
const value = this.getCellValue(params.node);
|
|
170
|
+
if (value === null || value === undefined || value === '') {
|
|
171
|
+
return this.model.includes(null);
|
|
172
|
+
}
|
|
173
|
+
return this.model.includes(String(value));
|
|
174
|
+
}
|
|
175
|
+
getModel() {
|
|
176
|
+
if (this.model === null) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return { filterType: 'set', values: [...this.model] };
|
|
180
|
+
}
|
|
181
|
+
setModel(model) {
|
|
182
|
+
this.model = (model === null || model === void 0 ? void 0 : model.values) ? [...model.values] : null;
|
|
183
|
+
this.syncCheckedStates();
|
|
184
|
+
}
|
|
185
|
+
getModelAsString(model) {
|
|
186
|
+
return formatEnumFilterModelAsString(model !== null && model !== void 0 ? model : this.getModel(), this.formatLabel);
|
|
187
|
+
}
|
|
188
|
+
/** Clears the filter back to inactive and notifies the grid; used by the floating filter. */
|
|
189
|
+
clear() {
|
|
190
|
+
if (this.model === null) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.model = null;
|
|
194
|
+
this.syncCheckedStates();
|
|
195
|
+
this.params.filterChangedCallback();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Surfaces the active selection in the column's header tooltip (alongside AG's native header
|
|
199
|
+
* filter icon), so the filter state is inspectable from the header without the layout-changing
|
|
200
|
+
* floating filter row. Installed as a `headerTooltipValueGetter`, which AG evaluates lazily on
|
|
201
|
+
* hover — no header refresh needed when the model changes. While inactive it returns
|
|
202
|
+
* `undefined`, which falls back to the consumer's own `headerTooltip`; when active, that
|
|
203
|
+
* tooltip is kept as the prefix. A consumer-provided getter is left untouched.
|
|
204
|
+
*/
|
|
205
|
+
installHeaderTooltip() {
|
|
206
|
+
const colDef = this.params.colDef;
|
|
207
|
+
if (!colDef || colDef.headerTooltipValueGetter) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.ownsHeaderTooltip = true;
|
|
211
|
+
colDef.headerTooltipValueGetter = () => {
|
|
212
|
+
const summary = formatEnumFilterModelAsString(this.getModel(), this.formatLabel);
|
|
213
|
+
if (!summary) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return colDef.headerTooltip
|
|
217
|
+
? `${colDef.headerTooltip} — Filtered: ${summary}`
|
|
218
|
+
: `Filtered: ${summary}`;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
afterGuiAttached(params) {
|
|
222
|
+
this.searchTerm = '';
|
|
223
|
+
this.eSearch.value = '';
|
|
224
|
+
this.renderList();
|
|
225
|
+
if (!(params === null || params === void 0 ? void 0 : params.suppressFocus)) {
|
|
226
|
+
this.eSearch.focus();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
refresh(params) {
|
|
230
|
+
this.params = params;
|
|
231
|
+
this.formatLabel = createEnumValueLabelFormatter(params);
|
|
232
|
+
// A refresh can carry a brand-new colDef object without the tooltip getter.
|
|
233
|
+
this.installHeaderTooltip();
|
|
234
|
+
this.resolveState();
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
destroy() {
|
|
238
|
+
var _a;
|
|
239
|
+
this.valuesRequestId += 1;
|
|
240
|
+
// Remove the tooltip getter this instance installed (never a consumer's): its closure holds
|
|
241
|
+
// the instance, and a successor filter created for the same colDef object would otherwise
|
|
242
|
+
// early-return in installHeaderTooltip and leave the header reporting the dead instance's
|
|
243
|
+
// frozen model.
|
|
244
|
+
if (this.ownsHeaderTooltip) {
|
|
245
|
+
delete this.params.colDef
|
|
246
|
+
.headerTooltipValueGetter;
|
|
247
|
+
this.ownsHeaderTooltip = false;
|
|
248
|
+
}
|
|
249
|
+
(_a = this.eGui) === null || _a === void 0 ? void 0 : _a.remove();
|
|
250
|
+
}
|
|
251
|
+
/** Resolves the option list (values array or getter) and re-renders. */
|
|
252
|
+
resolveState() {
|
|
253
|
+
var _a;
|
|
254
|
+
// Bound the loading flag's lifetime to this resolution attempt: a refresh() can supersede a
|
|
255
|
+
// pending async getter with synchronous values, and the superseded apply() never runs.
|
|
256
|
+
this.loading = false;
|
|
257
|
+
this.includeBlanks = this.params.blanks !== false;
|
|
258
|
+
const provided = this.params.values;
|
|
259
|
+
if (typeof provided === 'function') {
|
|
260
|
+
this.loadAsyncValues(provided);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(provided)) {
|
|
264
|
+
this.allValues = [...new Set(provided.map(String))];
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// An enum's option list is defined by its metadata, never derived from row data — a filter
|
|
268
|
+
// without values can only offer the blanks entry.
|
|
269
|
+
logger.warn(`EnumColumnFilter on column '${(_a = this.params.colDef) === null || _a === void 0 ? void 0 : _a.field}' has no ` +
|
|
270
|
+
'filterParams.values; provide the enum options (metadata enumOptions, an array, or a getter).');
|
|
271
|
+
this.allValues = [];
|
|
272
|
+
}
|
|
273
|
+
this.renderList();
|
|
274
|
+
}
|
|
275
|
+
loadAsyncValues(getValues) {
|
|
276
|
+
this.valuesRequestId += 1;
|
|
277
|
+
const requestId = this.valuesRequestId;
|
|
278
|
+
let applied = false;
|
|
279
|
+
const apply = (values) => {
|
|
280
|
+
if (applied || requestId !== this.valuesRequestId) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
applied = true;
|
|
284
|
+
this.loading = false;
|
|
285
|
+
this.allValues = [...new Set((values !== null && values !== void 0 ? values : []).map(String))];
|
|
286
|
+
this.renderList();
|
|
287
|
+
};
|
|
288
|
+
let result;
|
|
289
|
+
try {
|
|
290
|
+
result = getValues({ colDef: this.params.colDef, api: this.params.api, success: apply });
|
|
291
|
+
}
|
|
292
|
+
catch (_a) {
|
|
293
|
+
apply([]);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (applied) {
|
|
297
|
+
// The getter resolved synchronously through `success`.
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(result)) {
|
|
301
|
+
apply(result);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (result !== undefined) {
|
|
305
|
+
const thenable = result;
|
|
306
|
+
// A getter typed away from the contract can return neither an array, a thenable, nor
|
|
307
|
+
// undefined at runtime — treat that as no values rather than crashing on `.then`.
|
|
308
|
+
if (typeof thenable.then !== 'function') {
|
|
309
|
+
apply([]);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
this.loading = true;
|
|
313
|
+
this.renderList();
|
|
314
|
+
thenable.then(apply, () => apply([]));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// No return value: AG set-filter style — wait for the `success` callback.
|
|
318
|
+
this.loading = true;
|
|
319
|
+
this.renderList();
|
|
320
|
+
}
|
|
321
|
+
/** The raw field read covers params without `getValue` (e.g. stripped-down test params). */
|
|
322
|
+
getCellValue(node) {
|
|
323
|
+
var _a, _b;
|
|
324
|
+
const params = this.params;
|
|
325
|
+
if (typeof params.getValue === 'function') {
|
|
326
|
+
return params.getValue(node);
|
|
327
|
+
}
|
|
328
|
+
const field = (_a = params.colDef) === null || _a === void 0 ? void 0 : _a.field;
|
|
329
|
+
return field ? (_b = node === null || node === void 0 ? void 0 : node.data) === null || _b === void 0 ? void 0 : _b[field] : undefined;
|
|
330
|
+
}
|
|
331
|
+
/** Model values that are not in the option list, e.g. from a restored persisted model. */
|
|
332
|
+
extraModelValues() {
|
|
333
|
+
var _a;
|
|
334
|
+
return ((_a = this.model) !== null && _a !== void 0 ? _a : []).filter((value) => value !== null && !this.allValues.includes(value));
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* All offered entry keys: the option list, then any off-list model values (rendered so they can
|
|
338
|
+
* be deselected rather than being stuck in the model forever), then `null` (blanks) last like
|
|
339
|
+
* Excel. The blanks entry is also offered whenever the model references it, so a restored
|
|
340
|
+
* blanks selection stays visible and removable even before blank rows are detected.
|
|
341
|
+
*/
|
|
342
|
+
allKeys() {
|
|
343
|
+
var _a;
|
|
344
|
+
const keys = [...this.allValues, ...this.extraModelValues()];
|
|
345
|
+
if (this.includeBlanks || ((_a = this.model) === null || _a === void 0 ? void 0 : _a.includes(null))) {
|
|
346
|
+
keys.push(null);
|
|
347
|
+
}
|
|
348
|
+
return keys;
|
|
349
|
+
}
|
|
350
|
+
isSelected(key) {
|
|
351
|
+
return this.model === null || this.model.includes(key);
|
|
352
|
+
}
|
|
353
|
+
visibleKeys() {
|
|
354
|
+
const keys = this.allKeys();
|
|
355
|
+
if (!this.searchTerm) {
|
|
356
|
+
return keys;
|
|
357
|
+
}
|
|
358
|
+
const term = this.searchTerm.toLowerCase();
|
|
359
|
+
return keys.filter((key) => this.formatLabel(key).toLowerCase().includes(term) ||
|
|
360
|
+
(key !== null && key.toLowerCase().includes(term)));
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Applies a new selection. Collapses back to the inactive (`null`) model when every offered
|
|
364
|
+
* entry is selected, so a fully re-selected filter reports inactive exactly like the
|
|
365
|
+
* enterprise set filter. Selected values beyond the offered keys don't restrict anything an
|
|
366
|
+
* inactive filter would pass, so a superset selection collapses too.
|
|
367
|
+
*/
|
|
368
|
+
updateSelection(selected) {
|
|
369
|
+
const isAll = this.allKeys().every((key) => selected.has(key));
|
|
370
|
+
this.model = isAll ? null : [...selected];
|
|
371
|
+
this.syncCheckedStates();
|
|
372
|
+
this.params.filterChangedCallback();
|
|
373
|
+
}
|
|
374
|
+
currentSelection() {
|
|
375
|
+
return new Set(this.allKeys().filter((key) => this.isSelected(key)));
|
|
376
|
+
}
|
|
377
|
+
toggleValue(key, checked) {
|
|
378
|
+
const selected = this.currentSelection();
|
|
379
|
+
if (checked) {
|
|
380
|
+
selected.add(key);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
selected.delete(key);
|
|
384
|
+
}
|
|
385
|
+
this.updateSelection(selected);
|
|
386
|
+
}
|
|
387
|
+
/** Select-all acts on the searched subset only, mirroring Excel's AutoFilter. */
|
|
388
|
+
toggleAllVisible(checked) {
|
|
389
|
+
const visible = this.visibleKeys();
|
|
390
|
+
const selected = this.currentSelection();
|
|
391
|
+
visible.forEach((key) => (checked ? selected.add(key) : selected.delete(key)));
|
|
392
|
+
this.updateSelection(selected);
|
|
393
|
+
}
|
|
394
|
+
createGui() {
|
|
395
|
+
this.eGui = document.createElement('div');
|
|
396
|
+
this.eGui.className = CSS_PREFIX;
|
|
397
|
+
// Content lives in a shadow root so the FAST stylesheet travels with the element wherever
|
|
398
|
+
// AG attaches it — one adopted stylesheet shared across instances, nothing injected into
|
|
399
|
+
// foreign roots. Keydown is listened to inside the root, where targets are not retargeted.
|
|
400
|
+
this.eRoot = this.eGui.attachShadow({ mode: 'open' });
|
|
401
|
+
filterStyles.addStylesTo(this.eRoot);
|
|
402
|
+
this.eRoot.addEventListener('keydown', (event) => this.handleKeyDown(event));
|
|
403
|
+
this.eSearch = document.createElement('input');
|
|
404
|
+
this.eSearch.type = 'text';
|
|
405
|
+
this.eSearch.className = `${CSS_PREFIX}__search`;
|
|
406
|
+
this.eSearch.placeholder = 'Search';
|
|
407
|
+
this.eSearch.setAttribute('aria-label', 'Search filter values');
|
|
408
|
+
this.eSearch.addEventListener('input', () => {
|
|
409
|
+
this.searchTerm = this.eSearch.value.trim();
|
|
410
|
+
this.renderList();
|
|
411
|
+
});
|
|
412
|
+
this.eRoot.appendChild(this.eSearch);
|
|
413
|
+
const selectAllItem = this.createItem('(Select All)');
|
|
414
|
+
selectAllItem.label.classList.add(`${CSS_PREFIX}__select-all`);
|
|
415
|
+
this.eSelectAll = selectAllItem.input;
|
|
416
|
+
this.eSelectAll.addEventListener('change', () => {
|
|
417
|
+
this.toggleAllVisible(this.eSelectAll.checked);
|
|
418
|
+
});
|
|
419
|
+
this.eRoot.appendChild(selectAllItem.label);
|
|
420
|
+
this.eList = document.createElement('div');
|
|
421
|
+
this.eList.className = `${CSS_PREFIX}__list`;
|
|
422
|
+
this.eRoot.appendChild(this.eList);
|
|
423
|
+
}
|
|
424
|
+
createItem(text) {
|
|
425
|
+
const label = document.createElement('label');
|
|
426
|
+
label.className = `${CSS_PREFIX}__item`;
|
|
427
|
+
const input = document.createElement('input');
|
|
428
|
+
input.type = 'checkbox';
|
|
429
|
+
label.appendChild(input);
|
|
430
|
+
const span = document.createElement('span');
|
|
431
|
+
span.className = `${CSS_PREFIX}__item-label`;
|
|
432
|
+
span.textContent = text;
|
|
433
|
+
span.title = text;
|
|
434
|
+
label.appendChild(span);
|
|
435
|
+
return { label, input };
|
|
436
|
+
}
|
|
437
|
+
/** Rebuilds the value list; used when the values or the search term change. */
|
|
438
|
+
renderList() {
|
|
439
|
+
if (!this.eList) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
this.eList.replaceChildren();
|
|
443
|
+
this.itemInputs.clear();
|
|
444
|
+
const visible = this.visibleKeys();
|
|
445
|
+
if (this.loading || visible.length === 0) {
|
|
446
|
+
const empty = document.createElement('div');
|
|
447
|
+
empty.className = `${CSS_PREFIX}__no-matches`;
|
|
448
|
+
empty.textContent = this.loading ? 'Loading…' : 'No matches';
|
|
449
|
+
this.eList.appendChild(empty);
|
|
450
|
+
}
|
|
451
|
+
if (!this.loading) {
|
|
452
|
+
visible.forEach((key) => {
|
|
453
|
+
const { label, input } = this.createItem(this.formatLabel(key));
|
|
454
|
+
label.dataset.value = key === null ? '' : key;
|
|
455
|
+
input.addEventListener('change', () => this.toggleValue(key, input.checked));
|
|
456
|
+
this.itemInputs.set(key, input);
|
|
457
|
+
this.eList.appendChild(label);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
this.syncCheckedStates();
|
|
461
|
+
}
|
|
462
|
+
/** Refreshes checkbox states in place, preserving DOM (and keyboard focus) on toggles. */
|
|
463
|
+
syncCheckedStates() {
|
|
464
|
+
if (!this.eList) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
let selectedCount = 0;
|
|
468
|
+
this.itemInputs.forEach((input, key) => {
|
|
469
|
+
input.checked = this.isSelected(key);
|
|
470
|
+
if (input.checked) {
|
|
471
|
+
selectedCount += 1;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
const visibleCount = this.itemInputs.size;
|
|
475
|
+
this.eSelectAll.checked = visibleCount > 0 && selectedCount === visibleCount;
|
|
476
|
+
this.eSelectAll.indeterminate = selectedCount > 0 && selectedCount < visibleCount;
|
|
477
|
+
this.eSelectAll.disabled = visibleCount === 0;
|
|
478
|
+
}
|
|
479
|
+
focusables() {
|
|
480
|
+
return [this.eSearch, this.eSelectAll, ...this.itemInputs.values()];
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Arrow keys walk search box, "(Select All)" and the visible entries; Enter toggles the
|
|
484
|
+
* focused entry (Space is the checkbox's native toggle). Tab wraps within the popup: AG's own
|
|
485
|
+
* popup focus trap collects focusable elements with `querySelectorAll`, which does not pierce
|
|
486
|
+
* the shadow root, so the trap must live here.
|
|
487
|
+
*/
|
|
488
|
+
handleKeyDown(event) {
|
|
489
|
+
const target = event.target;
|
|
490
|
+
if (event.key === 'Enter' && target instanceof HTMLInputElement && target.type === 'checkbox') {
|
|
491
|
+
event.preventDefault();
|
|
492
|
+
target.checked = !target.checked;
|
|
493
|
+
target.dispatchEvent(new Event('change'));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (event.key === 'Tab') {
|
|
497
|
+
const focusables = this.focusables();
|
|
498
|
+
const first = focusables[0];
|
|
499
|
+
const last = focusables[focusables.length - 1];
|
|
500
|
+
if (event.shiftKey && target === first) {
|
|
501
|
+
event.preventDefault();
|
|
502
|
+
last.focus();
|
|
503
|
+
}
|
|
504
|
+
else if (!event.shiftKey && target === last) {
|
|
505
|
+
event.preventDefault();
|
|
506
|
+
first.focus();
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const focusables = this.focusables();
|
|
514
|
+
const currentIndex = focusables.indexOf(target);
|
|
515
|
+
if (currentIndex === -1) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const nextIndex = event.key === 'ArrowDown' ? currentIndex + 1 : currentIndex - 1;
|
|
519
|
+
if (nextIndex < 0 || nextIndex >= focusables.length) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
focusables[nextIndex].focus();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { css } from '@microsoft/fast-element';
|
|
2
|
+
import { createEnumValueLabelFormatter, formatEnumFilterModelAsString, } from './enum-column.filter';
|
|
3
|
+
const INACTIVE_SUMMARY = 'All';
|
|
4
|
+
const CSS_PREFIX = 'gp-enum-floating-filter';
|
|
5
|
+
/**
|
|
6
|
+
* Applied to each instance's shadow root; FAST shares one constructable stylesheet between them.
|
|
7
|
+
*/
|
|
8
|
+
const floatingFilterStyles = css `
|
|
9
|
+
:host {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 4px;
|
|
13
|
+
width: 100%;
|
|
14
|
+
min-width: 0;
|
|
15
|
+
color: var(--ag-foreground-color, inherit);
|
|
16
|
+
font: inherit;
|
|
17
|
+
}
|
|
18
|
+
.${CSS_PREFIX}__summary {
|
|
19
|
+
flex: 1;
|
|
20
|
+
min-width: 0;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
text-overflow: ellipsis;
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
opacity: 0.85;
|
|
26
|
+
}
|
|
27
|
+
.${CSS_PREFIX}__summary--inactive {
|
|
28
|
+
opacity: 0.5;
|
|
29
|
+
}
|
|
30
|
+
.${CSS_PREFIX}__clear {
|
|
31
|
+
flex: none;
|
|
32
|
+
border: none;
|
|
33
|
+
background: none;
|
|
34
|
+
color: inherit;
|
|
35
|
+
font: inherit;
|
|
36
|
+
line-height: 1;
|
|
37
|
+
padding: 2px 4px;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
border-radius: 3px;
|
|
40
|
+
opacity: 0.7;
|
|
41
|
+
}
|
|
42
|
+
.${CSS_PREFIX}__clear:hover {
|
|
43
|
+
opacity: 1;
|
|
44
|
+
background: var(--ag-row-hover-color, rgba(33, 150, 243, 0.1));
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
/**
|
|
48
|
+
* Floating filter companion for {@link EnumColumnFilter}: shows the current selection as a
|
|
49
|
+
* compact summary (e.g. `(2) Active, Pending`), opens the parent filter on click, and offers a
|
|
50
|
+
* one-click clear. Set via `ColDef.floatingFilterComponent` (see
|
|
51
|
+
* {@link GridProFilterTypes.enumFloatingFilter}); shown when the column enables `floatingFilter`.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export class EnumColumnFloatingFilter {
|
|
55
|
+
init(params) {
|
|
56
|
+
var _a, _b;
|
|
57
|
+
this.params = params;
|
|
58
|
+
this.formatLabel = createEnumValueLabelFormatter(params.filterParams);
|
|
59
|
+
this.eGui = document.createElement('div');
|
|
60
|
+
this.eGui.className = CSS_PREFIX;
|
|
61
|
+
// Content lives in a shadow root so the FAST stylesheet travels with the element wherever AG
|
|
62
|
+
// attaches it — floating filters get no attach hook, so styles cannot be injected on attach.
|
|
63
|
+
this.eRoot = this.eGui.attachShadow({ mode: 'open' });
|
|
64
|
+
floatingFilterStyles.addStylesTo(this.eRoot);
|
|
65
|
+
this.eSummary = document.createElement('span');
|
|
66
|
+
this.eSummary.className = `${CSS_PREFIX}__summary`;
|
|
67
|
+
this.eSummary.setAttribute('role', 'button');
|
|
68
|
+
this.eSummary.tabIndex = 0;
|
|
69
|
+
this.eSummary.addEventListener('click', () => this.params.showParentFilter());
|
|
70
|
+
this.eSummary.addEventListener('keydown', (event) => {
|
|
71
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
this.params.showParentFilter();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
this.eRoot.appendChild(this.eSummary);
|
|
77
|
+
this.eClear = document.createElement('button');
|
|
78
|
+
this.eClear.type = 'button';
|
|
79
|
+
this.eClear.className = `${CSS_PREFIX}__clear`;
|
|
80
|
+
this.eClear.textContent = '✕';
|
|
81
|
+
this.eClear.setAttribute('aria-label', 'Clear filter');
|
|
82
|
+
this.eClear.title = 'Clear filter';
|
|
83
|
+
this.eClear.hidden = true;
|
|
84
|
+
this.eClear.addEventListener('click', () => {
|
|
85
|
+
this.params.parentFilterInstance((instance) => {
|
|
86
|
+
var _a, _b;
|
|
87
|
+
(_b = (_a = instance).clear) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
this.eRoot.appendChild(this.eClear);
|
|
91
|
+
this.onParentModelChanged(((_b = (_a = params.currentParentModel) === null || _a === void 0 ? void 0 : _a.call(params)) !== null && _b !== void 0 ? _b : null));
|
|
92
|
+
}
|
|
93
|
+
getGui() {
|
|
94
|
+
return this.eGui;
|
|
95
|
+
}
|
|
96
|
+
onParentModelChanged(model) {
|
|
97
|
+
// Real text rather than CSS-generated content, so the inactive state is not silent for
|
|
98
|
+
// screen readers.
|
|
99
|
+
const summary = formatEnumFilterModelAsString(model, this.formatLabel) || INACTIVE_SUMMARY;
|
|
100
|
+
this.eSummary.textContent = summary;
|
|
101
|
+
this.eSummary.title = summary;
|
|
102
|
+
this.eSummary.classList.toggle(`${CSS_PREFIX}__summary--inactive`, !model);
|
|
103
|
+
this.eClear.hidden = !model;
|
|
104
|
+
}
|
|
105
|
+
destroy() {
|
|
106
|
+
var _a;
|
|
107
|
+
(_a = this.eGui) === null || _a === void 0 ? void 0 : _a.remove();
|
|
108
|
+
}
|
|
109
|
+
}
|