@gp-grid/angular 0.10.3 → 0.11.2

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.
@@ -1,243 +0,0 @@
1
- import type {
2
- ColumnDefinition,
3
- ColumnFilterModel,
4
- NumberFilterCondition,
5
- NumberFilterOperator,
6
- TextFilterCondition,
7
- TextFilterOperator,
8
- } from '@gp-grid/core';
9
-
10
- export interface TextConditionState {
11
- operator: string;
12
- value: string;
13
- nextOperator: 'and' | 'or';
14
- }
15
-
16
- export interface NumberConditionState {
17
- operator: string;
18
- value: string;
19
- valueTo: string;
20
- nextOperator: 'and' | 'or';
21
- }
22
-
23
- export type FilterMode = 'values' | 'condition';
24
-
25
- export const TEXT_OPERATORS: ReadonlyArray<{ value: TextFilterOperator; label: string }> = [
26
- { value: 'contains', label: 'Contains' },
27
- { value: 'notContains', label: 'Does not contain' },
28
- { value: 'equals', label: 'Equals' },
29
- { value: 'notEquals', label: 'Does not equal' },
30
- { value: 'startsWith', label: 'Starts with' },
31
- { value: 'endsWith', label: 'Ends with' },
32
- { value: 'blank', label: 'Is blank' },
33
- { value: 'notBlank', label: 'Is not blank' },
34
- ] as const;
35
-
36
- export const NUMBER_OPERATORS: ReadonlyArray<{ value: NumberFilterOperator; label: string }> = [
37
- { value: '=', label: 'Equals' },
38
- { value: '!=', label: 'Does not equal' },
39
- { value: '>', label: 'Greater than' },
40
- { value: '<', label: 'Less than' },
41
- { value: '>=', label: 'Greater than or equal' },
42
- { value: '<=', label: 'Less than or equal' },
43
- { value: 'between', label: 'Between' },
44
- { value: 'blank', label: 'Is blank' },
45
- { value: 'notBlank', label: 'Is not blank' },
46
- ] as const;
47
-
48
- const VALUE_LESS_TEXT_OPERATORS: ReadonlyArray<string> = ['blank', 'notBlank'];
49
- const VALUE_LESS_NUMBER_OPERATORS: ReadonlyArray<string> = ['blank', 'notBlank'];
50
-
51
- export const MAX_CHECKBOX_VALUES = 100;
52
-
53
- export const isValueLessTextOp = (operator: string): boolean =>
54
- VALUE_LESS_TEXT_OPERATORS.includes(operator);
55
-
56
- export const isValueLessNumberOp = (operator: string): boolean =>
57
- VALUE_LESS_NUMBER_OPERATORS.includes(operator);
58
-
59
- export const defaultTextCondition = (): TextConditionState => ({
60
- operator: 'contains',
61
- value: '',
62
- nextOperator: 'and',
63
- });
64
-
65
- export const defaultNumberCondition = (): NumberConditionState => ({
66
- operator: '=',
67
- value: '',
68
- valueTo: '',
69
- nextOperator: 'and',
70
- });
71
-
72
- export const computeUniqueValues = (distinctValues: ReadonlyArray<unknown>): string[] => {
73
- const seen = new Set<string>();
74
- const result: string[] = [];
75
- for (const val of distinctValues) {
76
- if (val === null || val === undefined || val === '') continue;
77
- const str = String(val);
78
- if (!seen.has(str)) {
79
- seen.add(str);
80
- result.push(str);
81
- }
82
- }
83
- return result.sort();
84
- };
85
-
86
- export interface TextInitState {
87
- filterMode: FilterMode;
88
- selectedValues: Set<string>;
89
- includeBlanks: boolean;
90
- textConditions: TextConditionState[];
91
- }
92
-
93
- export const initTextState = (
94
- filter: ColumnFilterModel | undefined,
95
- uniqueValues: string[],
96
- ): TextInitState => {
97
- if (!filter) return defaultTextState(uniqueValues);
98
-
99
- const textConds = filter.conditions.filter(
100
- (c): c is TextFilterCondition => c.type === 'text'
101
- );
102
- if (textConds.length === 0) return defaultTextState(uniqueValues);
103
-
104
- const firstCond = textConds[0];
105
- if (firstCond?.selectedValues !== undefined) {
106
- return {
107
- filterMode: 'values',
108
- selectedValues: new Set(firstCond.selectedValues),
109
- includeBlanks: firstCond.includeBlank ?? true,
110
- textConditions: [defaultTextCondition()],
111
- };
112
- }
113
-
114
- return {
115
- filterMode: 'condition',
116
- selectedValues: new Set(uniqueValues),
117
- includeBlanks: true,
118
- textConditions: textConds.map((c, i) => ({
119
- operator: c.operator,
120
- value: c.value ?? '',
121
- nextOperator: textConds[i]?.nextOperator ?? 'and',
122
- })),
123
- };
124
- };
125
-
126
- const defaultTextState = (uniqueValues: string[]): TextInitState => ({
127
- filterMode: 'values',
128
- selectedValues: new Set(uniqueValues),
129
- includeBlanks: true,
130
- textConditions: [defaultTextCondition()],
131
- });
132
-
133
- export const initNumberConditions = (
134
- filter: ColumnFilterModel | undefined,
135
- ): NumberConditionState[] => {
136
- if (!filter) return [defaultNumberCondition()];
137
-
138
- const numConds = filter.conditions.filter(
139
- (c): c is NumberFilterCondition => c.type === 'number'
140
- );
141
- if (numConds.length === 0) return [defaultNumberCondition()];
142
-
143
- return numConds.map((c, i) => ({
144
- operator: c.operator,
145
- value: c.value !== undefined ? String(c.value) : '',
146
- valueTo: c.valueTo !== undefined ? String(c.valueTo) : '',
147
- nextOperator: numConds[i]?.nextOperator ?? 'and',
148
- }));
149
- };
150
-
151
- export interface TextFilterInput {
152
- filterMode: FilterMode;
153
- uniqueValues: string[];
154
- selectedValues: Set<string>;
155
- includeBlanks: boolean;
156
- textConditions: TextConditionState[];
157
- }
158
-
159
- export const buildTextFilter = (input: TextFilterInput): ColumnFilterModel | null => {
160
- if (input.filterMode === 'values') {
161
- return buildValuesFilter(input);
162
- }
163
- return buildConditionTextFilter(input.textConditions);
164
- };
165
-
166
- const buildValuesFilter = (input: TextFilterInput): ColumnFilterModel | null => {
167
- const allSelected = input.uniqueValues.every(v => input.selectedValues.has(v));
168
- if (allSelected && input.includeBlanks) return null;
169
-
170
- return {
171
- conditions: [{
172
- type: 'text',
173
- operator: 'contains',
174
- selectedValues: new Set(input.selectedValues),
175
- includeBlank: input.includeBlanks,
176
- }],
177
- combination: 'or',
178
- };
179
- };
180
-
181
- const buildConditionTextFilter = (
182
- textConditions: TextConditionState[],
183
- ): ColumnFilterModel | null => {
184
- const conditions: TextFilterCondition[] = [];
185
- for (let i = 0; i < textConditions.length; i++) {
186
- const cond = textConditions[i];
187
- if (!cond) continue;
188
- if (!isValueLessTextOp(cond.operator) && !cond.value) continue;
189
-
190
- const out: TextFilterCondition = {
191
- type: 'text',
192
- operator: cond.operator as TextFilterOperator,
193
- };
194
- if (!isValueLessTextOp(cond.operator)) out.value = cond.value;
195
- linkNextOperator(conditions, i, textConditions);
196
- conditions.push(out);
197
- }
198
- if (conditions.length === 0) return null;
199
- return { conditions, combination: textConditions[0]?.nextOperator ?? 'and' };
200
- };
201
-
202
- export const buildNumberFilter = (
203
- numberConditions: NumberConditionState[],
204
- ): ColumnFilterModel | null => {
205
- const conditions: NumberFilterCondition[] = [];
206
- for (let i = 0; i < numberConditions.length; i++) {
207
- const cond = numberConditions[i];
208
- if (!cond) continue;
209
- if (!isValueLessNumberOp(cond.operator) && !cond.value) continue;
210
-
211
- const out: NumberFilterCondition = {
212
- type: 'number',
213
- operator: cond.operator as NumberFilterOperator,
214
- };
215
- if (!isValueLessNumberOp(cond.operator)) {
216
- out.value = parseFloat(cond.value);
217
- if (cond.operator === 'between' && cond.valueTo) {
218
- out.valueTo = parseFloat(cond.valueTo);
219
- }
220
- }
221
- linkNextOperator(conditions, i, numberConditions);
222
- conditions.push(out);
223
- }
224
- if (conditions.length === 0) return null;
225
- return { conditions, combination: numberConditions[0]?.nextOperator ?? 'and' };
226
- };
227
-
228
- const linkNextOperator = <T extends { nextOperator?: 'and' | 'or' }>(
229
- built: T[],
230
- currentIndex: number,
231
- source: ReadonlyArray<{ nextOperator: 'and' | 'or' }>,
232
- ): void => {
233
- const prev = built[built.length - 1];
234
- if (prev === undefined || currentIndex === 0) return;
235
- const prevSource = source[currentIndex - 1];
236
- prev.nextOperator = prevSource?.nextOperator ?? 'and';
237
- };
238
-
239
- export const resolveColId = (column: ColumnDefinition): string =>
240
- column.colId ?? column.field;
241
-
242
- export const isNumberColumn = (column: ColumnDefinition): boolean =>
243
- column.cellDataType === 'number';
@@ -1,236 +0,0 @@
1
- import {
2
- Component,
3
- ChangeDetectionStrategy,
4
- input,
5
- output,
6
- signal,
7
- effect,
8
- AfterViewInit,
9
- OnDestroy,
10
- ViewChild,
11
- ElementRef,
12
- HostListener,
13
- } from '@angular/core';
14
- import { calculateFilterPopupPosition } from '@gp-grid/core';
15
- import type {
16
- ColumnDefinition,
17
- CellValue,
18
- ColumnFilterModel,
19
- } from '@gp-grid/core';
20
- import { FILTER_POPUP_TEMPLATE } from './filter-popup.template';
21
- import {
22
- MAX_CHECKBOX_VALUES,
23
- NUMBER_OPERATORS,
24
- TEXT_OPERATORS,
25
- type FilterMode,
26
- type NumberConditionState,
27
- type TextConditionState,
28
- buildNumberFilter,
29
- buildTextFilter,
30
- computeUniqueValues,
31
- defaultNumberCondition,
32
- defaultTextCondition,
33
- initNumberConditions,
34
- initTextState,
35
- isNumberColumn as columnIsNumber,
36
- isValueLessNumberOp,
37
- isValueLessTextOp,
38
- resolveColId,
39
- } from './filter-popup/filter-logic';
40
-
41
- @Component({
42
- selector: 'gp-grid-filter-popup',
43
- standalone: true,
44
- imports: [],
45
- changeDetection: ChangeDetectionStrategy.Eager,
46
- template: FILTER_POPUP_TEMPLATE,
47
- })
48
- export class FilterPopupComponent implements AfterViewInit, OnDestroy {
49
- @ViewChild('popupEl', { static: false }) popupEl!: ElementRef<HTMLDivElement>;
50
-
51
- column = input.required<ColumnDefinition>();
52
- colIndex = input.required<number>();
53
- anchorEl = input.required<HTMLElement>();
54
- distinctValues = input.required<CellValue[]>();
55
- currentFilter = input<ColumnFilterModel | undefined>(undefined);
56
-
57
- apply = output<{ colId: string; filter: ColumnFilterModel | null }>();
58
- close = output<void>();
59
-
60
- popupTop = signal(0);
61
- popupLeft = signal(0);
62
- popupMinWidth = signal(200);
63
- positioned = signal(false);
64
-
65
- filterMode: FilterMode = 'values';
66
- searchText = '';
67
- selectedValues = new Set<string>();
68
- includeBlanks = true;
69
- textConditions: TextConditionState[] = [defaultTextCondition()];
70
- numberConditions: NumberConditionState[] = [defaultNumberCondition()];
71
-
72
- readonly textOperators = TEXT_OPERATORS;
73
- readonly numberOperators = NUMBER_OPERATORS;
74
-
75
- protected readonly isValueLessTextOp = isValueLessTextOp;
76
- protected readonly isValueLessNumberOp = isValueLessNumberOp;
77
-
78
- constructor() {
79
- effect(() => {
80
- this.anchorEl();
81
- this.currentFilter();
82
- this.initFromCurrentFilter();
83
- requestAnimationFrame(() => this.updatePosition());
84
- });
85
- }
86
-
87
- ngAfterViewInit(): void {
88
- requestAnimationFrame(() => {
89
- document.addEventListener('pointerdown', this.onDocumentPointerDown, true);
90
- });
91
- window.addEventListener('resize', this.onWindowResize);
92
- }
93
-
94
- ngOnDestroy(): void {
95
- document.removeEventListener('pointerdown', this.onDocumentPointerDown, true);
96
- window.removeEventListener('resize', this.onWindowResize);
97
- }
98
-
99
- @HostListener('keydown.escape')
100
- onEscape(): void {
101
- this.close.emit();
102
- }
103
-
104
- isNumberColumn(): boolean {
105
- return columnIsNumber(this.column());
106
- }
107
-
108
- showValuesMode(): boolean {
109
- return this.uniqueValues().length <= MAX_CHECKBOX_VALUES;
110
- }
111
-
112
- uniqueValues(): string[] {
113
- return computeUniqueValues(this.distinctValues());
114
- }
115
-
116
- filteredUniqueValues(): string[] {
117
- const search = this.searchText.toLowerCase();
118
- if (!search) return this.uniqueValues();
119
- return this.uniqueValues().filter(v => v.toLowerCase().includes(search));
120
- }
121
-
122
- toggleValue(val: string, checked: boolean): void {
123
- if (checked) {
124
- this.selectedValues.add(val);
125
- } else {
126
- this.selectedValues.delete(val);
127
- }
128
- }
129
-
130
- selectAll(): void {
131
- this.includeBlanks = true;
132
- for (const v of this.uniqueValues()) this.selectedValues.add(v);
133
- }
134
-
135
- deselectAll(): void {
136
- this.includeBlanks = false;
137
- this.selectedValues.clear();
138
- }
139
-
140
- onTextOperatorChange(index: number, value: string): void {
141
- setField(this.textConditions, index, 'operator', value);
142
- }
143
-
144
- onNumberOperatorChange(index: number, value: string): void {
145
- setField(this.numberConditions, index, 'operator', value);
146
- }
147
-
148
- addTextCondition(): void {
149
- this.textConditions.push(defaultTextCondition());
150
- }
151
-
152
- addNumberCondition(): void {
153
- this.numberConditions.push(defaultNumberCondition());
154
- }
155
-
156
- removeTextCondition(index: number): void {
157
- this.textConditions.splice(index, 1);
158
- }
159
-
160
- removeNumberCondition(index: number): void {
161
- this.numberConditions.splice(index, 1);
162
- }
163
-
164
- setTextNextOp(index: number, value: 'and' | 'or'): void {
165
- setField(this.textConditions, index, 'nextOperator', value);
166
- }
167
-
168
- setNumberNextOp(index: number, value: 'and' | 'or'): void {
169
- setField(this.numberConditions, index, 'nextOperator', value);
170
- }
171
-
172
- handleApply(): void {
173
- this.apply.emit({
174
- colId: resolveColId(this.column()),
175
- filter: this.buildFilter(),
176
- });
177
- }
178
-
179
- handleClear(): void {
180
- this.apply.emit({ colId: resolveColId(this.column()), filter: null });
181
- }
182
-
183
- private buildFilter(): ColumnFilterModel | null {
184
- if (this.isNumberColumn()) return buildNumberFilter(this.numberConditions);
185
- return buildTextFilter({
186
- filterMode: this.filterMode,
187
- uniqueValues: this.uniqueValues(),
188
- selectedValues: this.selectedValues,
189
- includeBlanks: this.includeBlanks,
190
- textConditions: this.textConditions,
191
- });
192
- }
193
-
194
- private initFromCurrentFilter(): void {
195
- const filter = this.currentFilter();
196
- if (this.isNumberColumn()) {
197
- this.numberConditions = initNumberConditions(filter);
198
- return;
199
- }
200
- const state = initTextState(filter, this.uniqueValues());
201
- this.filterMode = state.filterMode;
202
- this.selectedValues = state.selectedValues;
203
- this.includeBlanks = state.includeBlanks;
204
- this.textConditions = state.textConditions;
205
- }
206
-
207
- private updatePosition(): void {
208
- if (!this.popupEl?.nativeElement) return;
209
- const pos = calculateFilterPopupPosition(this.anchorEl(), this.popupEl.nativeElement);
210
- this.popupTop.set(pos.top);
211
- this.popupLeft.set(pos.left);
212
- this.popupMinWidth.set(pos.minWidth);
213
- this.positioned.set(true);
214
- }
215
-
216
- private onDocumentPointerDown = (event: Event): void => {
217
- const target = event.target as HTMLElement;
218
- if (target.closest('.gp-grid-filter-icon')) return;
219
- if (this.popupEl?.nativeElement?.contains(target)) return;
220
- this.close.emit();
221
- };
222
-
223
- private onWindowResize = (): void => {
224
- this.updatePosition();
225
- };
226
- }
227
-
228
- const setField = <T, K extends keyof T>(
229
- arr: T[],
230
- index: number,
231
- key: K,
232
- value: T[K],
233
- ): void => {
234
- const item = arr[index];
235
- if (item) item[key] = value;
236
- };
@@ -1,182 +0,0 @@
1
- export const FILTER_POPUP_TEMPLATE = `
2
- <div
3
- #popupEl
4
- class="gp-grid-filter-popup"
5
- [style.position]="'fixed'"
6
- [style.zIndex]="10000"
7
- [style.top.px]="popupTop()"
8
- [style.left.px]="popupLeft()"
9
- [style.minWidth.px]="popupMinWidth()"
10
- [style.visibility]="positioned() ? 'visible' : 'hidden'"
11
- (keydown.escape)="close.emit()"
12
- (click)="$event.stopPropagation()">
13
-
14
- <div class="gp-grid-filter-header">
15
- Filter: {{ column().headerName ?? column().field }}
16
- </div>
17
-
18
- <div [class]="'gp-grid-filter-content ' + (isNumberColumn() ? 'gp-grid-filter-number' : 'gp-grid-filter-text')">
19
- @if (isNumberColumn()) {
20
- @for (cond of numberConditions; track $index; let i = $index) {
21
- <div class="gp-grid-filter-condition">
22
- @if (i > 0) {
23
- <div class="gp-grid-filter-combination">
24
- <button
25
- type="button"
26
- [class.active]="numberConditions[i - 1]?.nextOperator === 'and'"
27
- (click)="setNumberNextOp(i - 1, 'and')">
28
- AND
29
- </button>
30
- <button
31
- type="button"
32
- [class.active]="numberConditions[i - 1]?.nextOperator === 'or'"
33
- (click)="setNumberNextOp(i - 1, 'or')">
34
- OR
35
- </button>
36
- </div>
37
- }
38
- <div class="gp-grid-filter-row">
39
- <select
40
- [value]="cond.operator"
41
- (change)="onNumberOperatorChange(i, $any($event.target).value)">
42
- @for (op of numberOperators; track op.value) {
43
- <option [value]="op.value">{{ op.label }}</option>
44
- }
45
- </select>
46
- @if (!isValueLessNumberOp(cond.operator)) {
47
- <input
48
- type="number"
49
- [value]="cond.value"
50
- (input)="cond.value = $any($event.target).value"
51
- placeholder="Value" />
52
- @if (cond.operator === 'between') {
53
- <span class="gp-grid-filter-to">to</span>
54
- <input
55
- type="number"
56
- [value]="cond.valueTo"
57
- (input)="cond.valueTo = $any($event.target).value"
58
- placeholder="Value" />
59
- }
60
- }
61
- @if (numberConditions.length > 1) {
62
- <button
63
- type="button"
64
- class="gp-grid-filter-remove"
65
- (click)="removeNumberCondition(i)">×</button>
66
- }
67
- </div>
68
- </div>
69
- }
70
- <button type="button" class="gp-grid-filter-add" (click)="addNumberCondition()">
71
- + Add condition
72
- </button>
73
- } @else {
74
- @if (showValuesMode()) {
75
- <div class="gp-grid-filter-mode-toggle">
76
- <button
77
- type="button"
78
- [class.active]="filterMode === 'values'"
79
- (click)="filterMode = 'values'">
80
- Values
81
- </button>
82
- <button
83
- type="button"
84
- [class.active]="filterMode === 'condition'"
85
- (click)="filterMode = 'condition'">
86
- Condition
87
- </button>
88
- </div>
89
- }
90
-
91
- @if (filterMode === 'values' && showValuesMode()) {
92
- <input
93
- class="gp-grid-filter-search"
94
- type="text"
95
- [value]="searchText"
96
- (input)="searchText = $any($event.target).value"
97
- placeholder="Search..." />
98
- <div class="gp-grid-filter-actions">
99
- <button type="button" (click)="selectAll()">Select All</button>
100
- <button type="button" (click)="deselectAll()">Deselect All</button>
101
- </div>
102
- <div class="gp-grid-filter-list">
103
- <label class="gp-grid-filter-option">
104
- <input
105
- type="checkbox"
106
- [checked]="includeBlanks"
107
- (change)="includeBlanks = $any($event.target).checked" />
108
- <span class="gp-grid-filter-blank">(Blanks)</span>
109
- </label>
110
- @for (val of filteredUniqueValues(); track val) {
111
- <label class="gp-grid-filter-option">
112
- <input
113
- type="checkbox"
114
- [checked]="selectedValues.has(val)"
115
- (change)="toggleValue(val, $any($event.target).checked)" />
116
- <span>{{ val }}</span>
117
- </label>
118
- }
119
- </div>
120
- }
121
-
122
- @if (filterMode === 'condition') {
123
- @for (cond of textConditions; track $index; let i = $index) {
124
- <div class="gp-grid-filter-condition">
125
- @if (i > 0) {
126
- <div class="gp-grid-filter-combination">
127
- <button
128
- type="button"
129
- [class.active]="textConditions[i - 1]?.nextOperator === 'and'"
130
- (click)="setTextNextOp(i - 1, 'and')">
131
- AND
132
- </button>
133
- <button
134
- type="button"
135
- [class.active]="textConditions[i - 1]?.nextOperator === 'or'"
136
- (click)="setTextNextOp(i - 1, 'or')">
137
- OR
138
- </button>
139
- </div>
140
- }
141
- <div class="gp-grid-filter-row">
142
- <select
143
- [value]="cond.operator"
144
- (change)="onTextOperatorChange(i, $any($event.target).value)">
145
- @for (op of textOperators; track op.value) {
146
- <option [value]="op.value">{{ op.label }}</option>
147
- }
148
- </select>
149
- @if (!isValueLessTextOp(cond.operator)) {
150
- <input
151
- class="gp-grid-filter-text-input"
152
- type="text"
153
- [value]="cond.value"
154
- (input)="cond.value = $any($event.target).value"
155
- placeholder="Value" />
156
- }
157
- @if (textConditions.length > 1) {
158
- <button
159
- type="button"
160
- class="gp-grid-filter-remove"
161
- (click)="removeTextCondition(i)">×</button>
162
- }
163
- </div>
164
- </div>
165
- }
166
- <button type="button" class="gp-grid-filter-add" (click)="addTextCondition()">
167
- + Add condition
168
- </button>
169
- }
170
- }
171
-
172
- <div class="gp-grid-filter-buttons">
173
- <button type="button" class="gp-grid-filter-btn-clear" (click)="handleClear()">
174
- Clear
175
- </button>
176
- <button type="button" class="gp-grid-filter-btn-apply" (click)="handleApply()">
177
- Apply
178
- </button>
179
- </div>
180
- </div>
181
- </div>
182
- `;