argent-grid 0.1.0 → 0.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/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +2 -2
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +91 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/package.json +20 -6
- package/plan.md +50 -18
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +327 -76
- package/src/lib/components/argent-grid.component.html +186 -64
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +642 -189
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.ts +302 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
- package/src/lib/rendering/canvas-renderer.ts +418 -305
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +46 -24
- package/src/lib/rendering/render/column-utils.ts +73 -0
- package/src/lib/rendering/render/hit-test.ts +55 -0
- package/src/lib/rendering/render/index.ts +79 -76
- package/src/lib/rendering/render/lines.ts +43 -43
- package/src/lib/rendering/render/primitives.ts +161 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +2 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +60 -50
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1165 -201
- package/src/lib/services/grid.service.ts +819 -187
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +73 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +188 -0
- package/src/stories/ArgentGrid.stories.ts +277 -0
- package/src/stories/Benchmark.stories.ts +74 -0
- package/src/stories/CellRenderers.stories.ts +221 -0
- package/src/stories/Filtering.stories.ts +252 -0
- package/src/stories/Grouping.stories.ts +217 -0
- package/src/stories/Theming.stories.ts +124 -0
- package/src/stories/benchmark-wrapper.component.ts +315 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { ArgentGridComponent } from './argent-grid.component';
|
|
5
|
+
import { ArgentGridModule } from '../argent-grid.module';
|
|
6
|
+
import { GridService } from '../services/grid.service';
|
|
7
|
+
|
|
8
|
+
interface TestData {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('ArgentGridComponent - Selection Behavior', () => {
|
|
14
|
+
let component: ArgentGridComponent<TestData>;
|
|
15
|
+
let fixture: ComponentFixture<ArgentGridComponent<TestData>>;
|
|
16
|
+
|
|
17
|
+
const testColumnDefs = [
|
|
18
|
+
{ field: 'id', headerName: 'ID', checkboxSelection: true },
|
|
19
|
+
{ field: 'name', headerName: 'Name' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const testRowData = [
|
|
23
|
+
{ id: 1, name: 'Item 1' },
|
|
24
|
+
{ id: 2, name: 'Item 2' },
|
|
25
|
+
{ id: 3, name: 'Item 3' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
await TestBed.configureTestingModule({
|
|
30
|
+
imports: [ArgentGridModule],
|
|
31
|
+
providers: [GridService, provideExperimentalZonelessChangeDetection()],
|
|
32
|
+
}).compileComponents();
|
|
33
|
+
|
|
34
|
+
fixture = TestBed.createComponent(ArgentGridComponent);
|
|
35
|
+
component = fixture.componentInstance;
|
|
36
|
+
component.columnDefs = testColumnDefs;
|
|
37
|
+
component.rowData = testRowData;
|
|
38
|
+
component.rowSelection = 'multiple';
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should initialize with no rows selected', () => {
|
|
43
|
+
expect(component.isAllSelected).toBe(false);
|
|
44
|
+
expect(component.isIndeterminateSelection).toBe(false);
|
|
45
|
+
expect(component.getApi().getSelectedRows().length).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should toggle all rows when header checkbox is clicked', () => {
|
|
49
|
+
// Select all
|
|
50
|
+
const event = { target: { checked: true } } as any;
|
|
51
|
+
component.onSelectionHeaderChange(event);
|
|
52
|
+
|
|
53
|
+
expect(component.getApi().getSelectedRows().length).toBe(3);
|
|
54
|
+
expect(component.isAllSelected).toBe(true);
|
|
55
|
+
expect(component.isIndeterminateSelection).toBe(false);
|
|
56
|
+
|
|
57
|
+
// Deselect all
|
|
58
|
+
event.target.checked = false;
|
|
59
|
+
component.onSelectionHeaderChange(event);
|
|
60
|
+
|
|
61
|
+
expect(component.getApi().getSelectedRows().length).toBe(0);
|
|
62
|
+
expect(component.isAllSelected).toBe(false);
|
|
63
|
+
expect(component.isIndeterminateSelection).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should update indeterminate state when single row is selected', () => {
|
|
67
|
+
const api = component.getApi();
|
|
68
|
+
const node = api.getDisplayedRowAtIndex(0);
|
|
69
|
+
|
|
70
|
+
node?.setSelected(true);
|
|
71
|
+
// GridState listener in component should trigger updateSelectionState
|
|
72
|
+
// We might need to manually trigger or wait for microtasks if not using zone.js
|
|
73
|
+
component.updateSelectionState();
|
|
74
|
+
|
|
75
|
+
expect(component.getApi().getSelectedRows().length).toBe(1);
|
|
76
|
+
expect(component.isAllSelected).toBe(false);
|
|
77
|
+
expect(component.isIndeterminateSelection).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should toggle selection when a row is clicked', () => {
|
|
81
|
+
const api = component.getApi();
|
|
82
|
+
const mouseEvent = new MouseEvent('click');
|
|
83
|
+
|
|
84
|
+
// Select row
|
|
85
|
+
component.onRowClick(0, mouseEvent);
|
|
86
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(true);
|
|
87
|
+
|
|
88
|
+
// Unselect row by clicking again (toggle behavior)
|
|
89
|
+
component.onRowClick(0, mouseEvent);
|
|
90
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should clear others when clicking a row without modifiers', () => {
|
|
94
|
+
const api = component.getApi();
|
|
95
|
+
const mouseEvent = new MouseEvent('click');
|
|
96
|
+
|
|
97
|
+
// Select first row
|
|
98
|
+
component.onRowClick(0, mouseEvent);
|
|
99
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(true);
|
|
100
|
+
|
|
101
|
+
// Click second row
|
|
102
|
+
component.onRowClick(1, mouseEvent);
|
|
103
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(false);
|
|
104
|
+
expect(api.getDisplayedRowAtIndex(1)?.selected).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should keep multiple selection when using Ctrl key', () => {
|
|
108
|
+
const api = component.getApi();
|
|
109
|
+
const ctrlEvent = new MouseEvent('click', { ctrlKey: true });
|
|
110
|
+
|
|
111
|
+
component.onRowClick(0, ctrlEvent);
|
|
112
|
+
component.onRowClick(1, ctrlEvent);
|
|
113
|
+
|
|
114
|
+
expect(api.getDisplayedRowAtIndex(0)?.selected).toBe(true);
|
|
115
|
+
expect(api.getDisplayedRowAtIndex(1)?.selected).toBe(true);
|
|
116
|
+
expect(api.getSelectedRows().length).toBe(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should disable sorting and menu for dedicated selection column', () => {
|
|
120
|
+
const selectionCol = component.getApi().getAllColumns()[0];
|
|
121
|
+
expect(selectionCol.colId).toBe('ag-Grid-SelectionColumn');
|
|
122
|
+
|
|
123
|
+
expect(component.isSortable(selectionCol)).toBe(false);
|
|
124
|
+
expect(component.hasHeaderMenu(selectionCol)).toBe(false);
|
|
125
|
+
expect(component.getHeaderName(selectionCol)).toBe('');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should allow resizing for dedicated selection column', () => {
|
|
129
|
+
const selectionCol = component.getApi().getAllColumns()[0];
|
|
130
|
+
expect(component.isResizable(selectionCol)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
Input,
|
|
6
|
+
OnInit,
|
|
7
|
+
Output,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'argent-set-filter',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [],
|
|
14
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
|
+
template: `
|
|
16
|
+
<div class="set-filter">
|
|
17
|
+
<!-- Search Box -->
|
|
18
|
+
<div class="set-filter-search">
|
|
19
|
+
<input
|
|
20
|
+
type="text"
|
|
21
|
+
[value]="searchText"
|
|
22
|
+
(input)="onSearchInput($event)"
|
|
23
|
+
placeholder="Search..."
|
|
24
|
+
class="set-filter-input"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Select All / Clear All -->
|
|
29
|
+
<div class="set-filter-actions">
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
(click)="selectAll()"
|
|
33
|
+
class="set-filter-btn"
|
|
34
|
+
>
|
|
35
|
+
Select All
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
(click)="clearAll()"
|
|
40
|
+
class="set-filter-btn"
|
|
41
|
+
>
|
|
42
|
+
Clear All
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Checkbox List -->
|
|
47
|
+
<div class="set-filter-list">
|
|
48
|
+
@for (item of filteredValues; track item.value) {
|
|
49
|
+
<label class="set-filter-item">
|
|
50
|
+
<input
|
|
51
|
+
type="checkbox"
|
|
52
|
+
[checked]="item.selected"
|
|
53
|
+
(change)="onValueToggled(item.value, $event)"
|
|
54
|
+
/>
|
|
55
|
+
<span class="set-filter-label">{{ item.displayValue }}</span>
|
|
56
|
+
<span class="set-filter-count">({{ item.count }})</span>
|
|
57
|
+
</label>
|
|
58
|
+
} @empty {
|
|
59
|
+
<div class="set-filter-empty">
|
|
60
|
+
No values found
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Action Buttons -->
|
|
66
|
+
<div class="set-filter-footer">
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
(click)="applyFilter()"
|
|
70
|
+
class="set-filter-apply"
|
|
71
|
+
>
|
|
72
|
+
Apply
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
(click)="resetFilter()"
|
|
77
|
+
class="set-filter-reset"
|
|
78
|
+
>
|
|
79
|
+
Reset
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
`,
|
|
84
|
+
styles: [
|
|
85
|
+
`
|
|
86
|
+
.set-filter {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
width: 250px;
|
|
90
|
+
max-height: 400px;
|
|
91
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
92
|
+
font-size: 13px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.set-filter-search {
|
|
96
|
+
padding: 8px;
|
|
97
|
+
border-bottom: 1px solid #e0e0e0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.set-filter-input {
|
|
101
|
+
width: 100%;
|
|
102
|
+
padding: 6px 10px;
|
|
103
|
+
border: 1px solid #d0d0d0;
|
|
104
|
+
border-radius: 4px;
|
|
105
|
+
font-size: 13px;
|
|
106
|
+
box-sizing: border-box;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.set-filter-input:focus {
|
|
110
|
+
outline: none;
|
|
111
|
+
border-color: #4f46e5;
|
|
112
|
+
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.set-filter-actions {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: 8px;
|
|
118
|
+
padding: 8px;
|
|
119
|
+
border-bottom: 1px solid #e0e0e0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.set-filter-btn {
|
|
123
|
+
flex: 1;
|
|
124
|
+
padding: 4px 8px;
|
|
125
|
+
border: 1px solid #d0d0d0;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
background: #fff;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
font-size: 12px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.set-filter-btn:hover {
|
|
133
|
+
background: #f5f5f5;
|
|
134
|
+
border-color: #4f46e5;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.set-filter-list {
|
|
138
|
+
flex: 1;
|
|
139
|
+
overflow-y: auto;
|
|
140
|
+
padding: 8px 0;
|
|
141
|
+
max-height: 250px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.set-filter-item {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
padding: 4px 12px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
gap: 8px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.set-filter-item:hover {
|
|
153
|
+
background: #f5f5f5;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.set-filter-item input[type="checkbox"] {
|
|
157
|
+
margin: 0;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.set-filter-label {
|
|
162
|
+
flex: 1;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
text-overflow: ellipsis;
|
|
165
|
+
white-space: nowrap;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.set-filter-count {
|
|
169
|
+
color: #999;
|
|
170
|
+
font-size: 11px;
|
|
171
|
+
margin-left: 4px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.set-filter-empty {
|
|
175
|
+
padding: 16px;
|
|
176
|
+
text-align: center;
|
|
177
|
+
color: #999;
|
|
178
|
+
font-style: italic;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.set-filter-footer {
|
|
182
|
+
display: flex;
|
|
183
|
+
gap: 8px;
|
|
184
|
+
padding: 8px;
|
|
185
|
+
border-top: 1px solid #e0e0e0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.set-filter-apply,
|
|
189
|
+
.set-filter-reset {
|
|
190
|
+
flex: 1;
|
|
191
|
+
padding: 6px 12px;
|
|
192
|
+
border: none;
|
|
193
|
+
border-radius: 4px;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.set-filter-apply {
|
|
199
|
+
background: #4f46e5;
|
|
200
|
+
color: #fff;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.set-filter-apply:hover {
|
|
204
|
+
background: #4338ca;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.set-filter-reset {
|
|
208
|
+
background: #f5f5f5;
|
|
209
|
+
color: #333;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.set-filter-reset:hover {
|
|
213
|
+
background: #e5e5e5;
|
|
214
|
+
}
|
|
215
|
+
`,
|
|
216
|
+
],
|
|
217
|
+
})
|
|
218
|
+
export class SetFilterComponent<T = any> implements OnInit {
|
|
219
|
+
@Input() values: T[] = [];
|
|
220
|
+
@Input() valueFormatter?: (value: T) => string;
|
|
221
|
+
@Output() filterChanged = new EventEmitter<T[]>();
|
|
222
|
+
|
|
223
|
+
searchText = '';
|
|
224
|
+
selectedValues: T[] = [];
|
|
225
|
+
allValues: Array<{ value: T; displayValue: string; count: number; selected: boolean }> = [];
|
|
226
|
+
|
|
227
|
+
get filteredValues() {
|
|
228
|
+
if (!this.searchText) {
|
|
229
|
+
return this.allValues;
|
|
230
|
+
}
|
|
231
|
+
const search = this.searchText.toLowerCase();
|
|
232
|
+
return this.allValues.filter((item) => item.displayValue.toLowerCase().includes(search));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
ngOnInit(): void {
|
|
236
|
+
this.initializeValues();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
onSearchInput(event: Event): void {
|
|
240
|
+
this.searchText = (event.target as HTMLInputElement).value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private initializeValues(): void {
|
|
244
|
+
// Count occurrences of each value
|
|
245
|
+
const valueCounts = new Map<T, number>();
|
|
246
|
+
this.values.forEach((value) => {
|
|
247
|
+
valueCounts.set(value, (valueCounts.get(value) || 0) + 1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Build value list with counts
|
|
251
|
+
this.allValues = Array.from(valueCounts.entries()).map(([value, count]) => ({
|
|
252
|
+
value,
|
|
253
|
+
displayValue: this.valueFormatter ? this.valueFormatter(value) : String(value),
|
|
254
|
+
count,
|
|
255
|
+
selected: true, // All selected by default
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
this.selectedValues = this.values.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
onSearchChanged(): void {
|
|
262
|
+
// Trigger change detection through filteredValues getter
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
onValueToggled(value: T, event: Event): void {
|
|
266
|
+
const checkbox = event.target as HTMLInputElement;
|
|
267
|
+
const item = this.allValues.find((v) => v.value === value);
|
|
268
|
+
if (item) {
|
|
269
|
+
item.selected = checkbox.checked;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.updateSelectedValues();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
selectAll(): void {
|
|
276
|
+
this.allValues.forEach((item) => {
|
|
277
|
+
item.selected = true;
|
|
278
|
+
});
|
|
279
|
+
this.updateSelectedValues();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
clearAll(): void {
|
|
283
|
+
this.allValues.forEach((item) => {
|
|
284
|
+
item.selected = false;
|
|
285
|
+
});
|
|
286
|
+
this.updateSelectedValues();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private updateSelectedValues(): void {
|
|
290
|
+
this.selectedValues = this.allValues.filter((item) => item.selected).map((item) => item.value);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
applyFilter(): void {
|
|
294
|
+
this.filterChanged.emit(this.selectedValues);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
resetFilter(): void {
|
|
298
|
+
this.searchText = '';
|
|
299
|
+
this.initializeValues();
|
|
300
|
+
this.filterChanged.emit(this.selectedValues);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Directive,
|
|
3
|
-
Input,
|
|
4
|
-
OnInit,
|
|
5
|
-
OnDestroy,
|
|
6
|
-
ComponentRef,
|
|
7
|
-
ViewContainerRef,
|
|
8
|
-
ComponentFactoryResolver
|
|
9
|
-
} from '@angular/core';
|
|
10
|
-
import { GridOptions, ColDef, ColGroupDef } from '../types/ag-grid-types';
|
|
1
|
+
import { ComponentRef, Directive, Input, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
|
|
11
2
|
import { ArgentGridComponent } from '../components/argent-grid.component';
|
|
3
|
+
import { ColDef, ColGroupDef, GridOptions } from '../types/ag-grid-types';
|
|
12
4
|
|
|
13
5
|
/**
|
|
14
6
|
* AgGridCompatibilityDirective - Drop-in replacement for AG Grid
|
|
15
|
-
*
|
|
7
|
+
*
|
|
16
8
|
* This directive allows users to migrate from AG Grid to ArgentGrid
|
|
17
9
|
* by simply changing their import statement. It maps AG Grid's
|
|
18
10
|
* [columnDefs] and [rowData] inputs to ArgentGrid's internal format.
|
|
19
11
|
*/
|
|
20
12
|
@Directive({
|
|
21
|
-
selector: '[argentGrid], ag-grid-angular'
|
|
13
|
+
selector: '[argentGrid], ag-grid-angular',
|
|
22
14
|
})
|
|
23
15
|
export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDestroy {
|
|
24
16
|
@Input('columnDefs') set columnDefs(value: (ColDef<TData> | ColGroupDef<TData>)[] | null) {
|
|
@@ -29,7 +21,7 @@ export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDest
|
|
|
29
21
|
return this._columnDefs;
|
|
30
22
|
}
|
|
31
23
|
private _columnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
|
|
32
|
-
|
|
24
|
+
|
|
33
25
|
@Input('rowData') set rowData(value: TData[] | null) {
|
|
34
26
|
this._rowData = value;
|
|
35
27
|
this.updateGrid();
|
|
@@ -38,7 +30,7 @@ export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDest
|
|
|
38
30
|
return this._rowData;
|
|
39
31
|
}
|
|
40
32
|
private _rowData: TData[] | null = null;
|
|
41
|
-
|
|
33
|
+
|
|
42
34
|
@Input('gridOptions') set gridOptions(value: GridOptions<TData>) {
|
|
43
35
|
this._gridOptions = value;
|
|
44
36
|
this.updateGrid();
|
|
@@ -47,42 +39,40 @@ export class AgGridCompatibilityDirective<TData = any> implements OnInit, OnDest
|
|
|
47
39
|
return this._gridOptions;
|
|
48
40
|
}
|
|
49
41
|
private _gridOptions: GridOptions<TData> | null = null;
|
|
50
|
-
|
|
42
|
+
|
|
51
43
|
private gridComponentRef: ComponentRef<ArgentGridComponent<TData>> | null = null;
|
|
52
|
-
|
|
53
|
-
constructor(
|
|
54
|
-
|
|
55
|
-
) {}
|
|
56
|
-
|
|
44
|
+
|
|
45
|
+
constructor(private viewContainerRef: ViewContainerRef) {}
|
|
46
|
+
|
|
57
47
|
ngOnInit(): void {
|
|
58
48
|
this.createGridComponent();
|
|
59
49
|
}
|
|
60
|
-
|
|
50
|
+
|
|
61
51
|
ngOnDestroy(): void {
|
|
62
52
|
if (this.gridComponentRef) {
|
|
63
53
|
this.gridComponentRef.destroy();
|
|
64
54
|
}
|
|
65
55
|
}
|
|
66
|
-
|
|
56
|
+
|
|
67
57
|
private createGridComponent(): void {
|
|
68
58
|
// Create ArgentGrid component
|
|
69
59
|
this.gridComponentRef = this.viewContainerRef.createComponent(ArgentGridComponent as any);
|
|
70
|
-
|
|
60
|
+
|
|
71
61
|
this.updateGrid();
|
|
72
62
|
}
|
|
73
|
-
|
|
63
|
+
|
|
74
64
|
private updateGrid(): void {
|
|
75
65
|
if (!this.gridComponentRef) {
|
|
76
66
|
return;
|
|
77
67
|
}
|
|
78
|
-
|
|
68
|
+
|
|
79
69
|
// Map AG Grid inputs to ArgentGrid component
|
|
80
70
|
this.gridComponentRef.instance.columnDefs = this.columnDefs;
|
|
81
71
|
this.gridComponentRef.instance.rowData = this.rowData;
|
|
82
72
|
this.gridComponentRef.instance.gridOptions = this.gridOptions || undefined;
|
|
83
73
|
this.gridComponentRef.instance.refresh();
|
|
84
74
|
}
|
|
85
|
-
|
|
75
|
+
|
|
86
76
|
/**
|
|
87
77
|
* Get the underlying GridApi for programmatic access
|
|
88
78
|
*/
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[clickOutside]',
|
|
5
|
+
standalone: true,
|
|
6
|
+
})
|
|
7
|
+
export class ClickOutsideDirective {
|
|
8
|
+
@Output() clickOutside = new EventEmitter<void>();
|
|
9
|
+
|
|
10
|
+
constructor(private elementRef: ElementRef) {}
|
|
11
|
+
|
|
12
|
+
@HostListener('document:click', ['$event.target'])
|
|
13
|
+
onClick(target: HTMLElement): void {
|
|
14
|
+
const clickedInside = this.elementRef.nativeElement.contains(target);
|
|
15
|
+
if (!clickedInside) {
|
|
16
|
+
this.clickOutside.emit();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|