@alaarab/ogrid-angular-radix 2.0.4

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.
Files changed (33) hide show
  1. package/README.md +76 -0
  2. package/dist/esm/column-chooser/column-chooser.component.js +199 -0
  3. package/dist/esm/column-header-filter/column-header-filter.component.js +497 -0
  4. package/dist/esm/datagrid-table/datagrid-table.component.js +573 -0
  5. package/dist/esm/index.js +14 -0
  6. package/dist/esm/ogrid/ogrid.component.js +77 -0
  7. package/dist/esm/pagination-controls/pagination-controls.component.js +189 -0
  8. package/dist/types/column-chooser/column-chooser.component.d.ts +26 -0
  9. package/dist/types/column-header-filter/column-header-filter.component.d.ts +67 -0
  10. package/dist/types/datagrid-table/datagrid-table.component.d.ts +131 -0
  11. package/dist/types/index.d.ts +12 -0
  12. package/dist/types/ogrid/ogrid.component.d.ts +14 -0
  13. package/dist/types/pagination-controls/pagination-controls.component.d.ts +15 -0
  14. package/jest-mocks/angular-cdk-overlay.cjs.js +38 -0
  15. package/jest-mocks/style-mock.js +1 -0
  16. package/jest.config.js +43 -0
  17. package/package.json +37 -0
  18. package/scripts/compile-styles.js +53 -0
  19. package/src/__tests__/column-chooser.component.spec.ts.skip +195 -0
  20. package/src/__tests__/column-header-filter.component.spec.ts.skip +401 -0
  21. package/src/__tests__/datagrid-table.component.spec.ts.skip +417 -0
  22. package/src/__tests__/exports.test.ts +54 -0
  23. package/src/__tests__/ogrid.component.spec.ts.skip +236 -0
  24. package/src/__tests__/pagination-controls.component.spec.ts.skip +190 -0
  25. package/src/column-chooser/column-chooser.component.ts +204 -0
  26. package/src/column-header-filter/column-header-filter.component.ts +528 -0
  27. package/src/datagrid-table/datagrid-table.component.scss +289 -0
  28. package/src/datagrid-table/datagrid-table.component.ts +636 -0
  29. package/src/index.ts +16 -0
  30. package/src/ogrid/ogrid.component.ts +78 -0
  31. package/src/pagination-controls/pagination-controls.component.ts +187 -0
  32. package/tsconfig.build.json +9 -0
  33. package/tsconfig.json +21 -0
@@ -0,0 +1,497 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Component, input, signal, computed, ChangeDetectionStrategy, viewChild, } from '@angular/core';
8
+ /**
9
+ * Column header filter component for Angular Radix (lightweight styling).
10
+ * Standalone component with inline template and positioned popovers.
11
+ */
12
+ let ColumnHeaderFilterComponent = class ColumnHeaderFilterComponent {
13
+ constructor() {
14
+ this.columnKey = input.required();
15
+ this.columnName = input.required();
16
+ this.filterType = input.required();
17
+ this.isSorted = input(false);
18
+ this.isSortedDescending = input(false);
19
+ this.onSort = input(undefined);
20
+ this.selectedValues = input([]);
21
+ this.onFilterChange = input(undefined);
22
+ this.options = input(undefined);
23
+ this.isLoadingOptions = input(false);
24
+ this.textValue = input('');
25
+ this.onTextChange = input(undefined);
26
+ this.selectedUser = input(undefined);
27
+ this.onUserChange = input(undefined);
28
+ this.peopleSearch = input(undefined);
29
+ this.dateValue = input(undefined);
30
+ this.onDateChange = input(undefined);
31
+ this.headerRef = viewChild('headerEl');
32
+ this.isFilterOpen = signal(false);
33
+ this.popoverTop = signal(0);
34
+ this.popoverLeft = signal(0);
35
+ // Text filter
36
+ this.tempTextValue = signal('');
37
+ // MultiSelect filter
38
+ this.searchText = signal('');
39
+ this.tempSelected = signal(new Set());
40
+ // Date filter
41
+ this.tempDateFrom = signal('');
42
+ this.tempDateTo = signal('');
43
+ this.hasActiveFilter = computed(() => {
44
+ const ft = this.filterType();
45
+ if (ft === 'text')
46
+ return !!this.textValue();
47
+ if (ft === 'multiSelect')
48
+ return (this.selectedValues() ?? []).length > 0;
49
+ if (ft === 'date') {
50
+ const dv = this.dateValue();
51
+ return !!dv && (!!dv.from || !!dv.to);
52
+ }
53
+ return false;
54
+ });
55
+ this.filteredOptions = computed(() => {
56
+ const search = this.searchText().toLowerCase();
57
+ const opts = this.options() ?? [];
58
+ if (!search)
59
+ return opts;
60
+ return opts.filter(o => o.toLowerCase().includes(search));
61
+ });
62
+ }
63
+ toggleFilter(event) {
64
+ event.stopPropagation();
65
+ if (this.isFilterOpen()) {
66
+ this.isFilterOpen.set(false);
67
+ return;
68
+ }
69
+ // Initialize temp values
70
+ if (this.filterType() === 'text') {
71
+ this.tempTextValue.set(this.textValue() ?? '');
72
+ }
73
+ else if (this.filterType() === 'multiSelect') {
74
+ this.tempSelected.set(new Set(this.selectedValues() ?? []));
75
+ this.searchText.set('');
76
+ }
77
+ else if (this.filterType() === 'date') {
78
+ const dv = this.dateValue();
79
+ this.tempDateFrom.set(dv?.from ?? '');
80
+ this.tempDateTo.set(dv?.to ?? '');
81
+ }
82
+ // Calculate popover position
83
+ const headerEl = this.headerRef()?.nativeElement;
84
+ if (headerEl) {
85
+ const rect = headerEl.getBoundingClientRect();
86
+ this.popoverTop.set(rect.bottom + 4);
87
+ this.popoverLeft.set(rect.left);
88
+ }
89
+ this.isFilterOpen.set(true);
90
+ }
91
+ onDocumentClick(event) {
92
+ const el = event.target;
93
+ if (!el.closest('column-header-filter')) {
94
+ this.isFilterOpen.set(false);
95
+ }
96
+ }
97
+ asInputValue(event) {
98
+ return event.target.value;
99
+ }
100
+ // Text filter handlers
101
+ onTextKeydown(event) {
102
+ if (event.key === 'Enter') {
103
+ this.handleTextApply();
104
+ }
105
+ else if (event.key === 'Escape') {
106
+ this.isFilterOpen.set(false);
107
+ }
108
+ }
109
+ handleTextApply() {
110
+ this.onTextChange()?.(this.tempTextValue());
111
+ this.isFilterOpen.set(false);
112
+ }
113
+ handleTextClear() {
114
+ this.tempTextValue.set('');
115
+ this.onTextChange()?.('');
116
+ this.isFilterOpen.set(false);
117
+ }
118
+ // MultiSelect filter handlers
119
+ handleCheckboxChange(option, event) {
120
+ const checked = event.target.checked;
121
+ const newSet = new Set(this.tempSelected());
122
+ if (checked) {
123
+ newSet.add(option);
124
+ }
125
+ else {
126
+ newSet.delete(option);
127
+ }
128
+ this.tempSelected.set(newSet);
129
+ }
130
+ handleSelectAllFiltered() {
131
+ const newSet = new Set(this.tempSelected());
132
+ for (const opt of this.filteredOptions()) {
133
+ newSet.add(opt);
134
+ }
135
+ this.tempSelected.set(newSet);
136
+ }
137
+ handleClearSelection() {
138
+ this.tempSelected.set(new Set());
139
+ }
140
+ handleMultiSelectApply() {
141
+ this.onFilterChange()?.([...this.tempSelected()]);
142
+ this.isFilterOpen.set(false);
143
+ }
144
+ handleMultiSelectClear() {
145
+ this.tempSelected.set(new Set());
146
+ this.onFilterChange()?.([]);
147
+ this.isFilterOpen.set(false);
148
+ }
149
+ // Date filter handlers
150
+ handleDateApply() {
151
+ const from = this.tempDateFrom();
152
+ const to = this.tempDateTo();
153
+ if (from || to) {
154
+ this.onDateChange()?.({ from, to });
155
+ }
156
+ this.isFilterOpen.set(false);
157
+ }
158
+ handleDateClear() {
159
+ this.tempDateFrom.set('');
160
+ this.tempDateTo.set('');
161
+ this.onDateChange()?.({ from: '', to: '' });
162
+ this.isFilterOpen.set(false);
163
+ }
164
+ };
165
+ ColumnHeaderFilterComponent = __decorate([
166
+ Component({
167
+ selector: 'column-header-filter',
168
+ standalone: true,
169
+ changeDetection: ChangeDetectionStrategy.OnPush,
170
+ template: `
171
+ <div class="ogrid-header-filter" #headerEl>
172
+ <div class="ogrid-header-filter__label">
173
+ <span class="ogrid-header-filter__name" [title]="columnName()" data-header-label>
174
+ {{ columnName() }}
175
+ </span>
176
+ </div>
177
+
178
+ <div class="ogrid-header-filter__actions">
179
+ @if (onSort()) {
180
+ <button
181
+ class="ogrid-header-filter__btn"
182
+ [class.ogrid-header-filter__btn--active]="isSorted()"
183
+ (click)="onSort()!()"
184
+ [attr.aria-label]="'Sort by ' + columnName()"
185
+ [title]="isSorted() ? (isSortedDescending() ? 'Sorted descending' : 'Sorted ascending') : 'Sort'"
186
+ >
187
+ @if (isSorted() && isSortedDescending()) {
188
+
189
+ } @else if (isSorted()) {
190
+
191
+ } @else {
192
+
193
+ }
194
+ </button>
195
+ }
196
+
197
+ @if (filterType() !== 'none') {
198
+ <button
199
+ class="ogrid-header-filter__btn"
200
+ [class.ogrid-header-filter__btn--active]="hasActiveFilter() || isFilterOpen()"
201
+ (click)="toggleFilter($event)"
202
+ [attr.aria-label]="'Filter ' + columnName()"
203
+ [title]="'Filter ' + columnName()"
204
+ >
205
+
206
+ @if (hasActiveFilter()) {
207
+ <span class="ogrid-header-filter__dot"></span>
208
+ }
209
+ </button>
210
+ }
211
+ </div>
212
+ </div>
213
+
214
+ @if (isFilterOpen() && filterType() !== 'none') {
215
+ <div
216
+ class="ogrid-header-filter__popover"
217
+ [style.top.px]="popoverTop()"
218
+ [style.left.px]="popoverLeft()"
219
+ (click)="$event.stopPropagation()"
220
+ >
221
+ <div class="ogrid-header-filter__popover-header">
222
+ Filter: {{ columnName() }}
223
+ </div>
224
+
225
+ @switch (filterType()) {
226
+ @case ('text') {
227
+ <div class="ogrid-header-filter__popover-body" style="width: 260px;">
228
+ <div style="padding: 12px;">
229
+ <input
230
+ type="text"
231
+ class="ogrid-header-filter__input"
232
+ placeholder="Enter search term..."
233
+ [value]="tempTextValue()"
234
+ (input)="tempTextValue.set(asInputValue($event))"
235
+ (keydown)="onTextKeydown($event)"
236
+ autocomplete="off"
237
+ />
238
+ </div>
239
+ <div class="ogrid-header-filter__popover-actions">
240
+ <button class="ogrid-header-filter__action-btn" [disabled]="!tempTextValue()" (click)="handleTextClear()">Clear</button>
241
+ <button class="ogrid-header-filter__action-btn ogrid-header-filter__action-btn--primary" (click)="handleTextApply()">Apply</button>
242
+ </div>
243
+ </div>
244
+ }
245
+ @case ('multiSelect') {
246
+ <div class="ogrid-header-filter__popover-body" style="width: 280px;">
247
+ <div style="padding: 12px 12px 4px;">
248
+ <input
249
+ type="text"
250
+ class="ogrid-header-filter__input"
251
+ placeholder="Search..."
252
+ [value]="searchText()"
253
+ (input)="searchText.set(asInputValue($event))"
254
+ (keydown)="$event.stopPropagation()"
255
+ autocomplete="off"
256
+ />
257
+ <div class="ogrid-header-filter__options-info">
258
+ {{ filteredOptions().length }} of {{ (options() ?? []).length }} options
259
+ </div>
260
+ </div>
261
+ <div class="ogrid-header-filter__select-actions">
262
+ <button class="ogrid-header-filter__action-btn" (click)="handleSelectAllFiltered()">
263
+ Select All ({{ filteredOptions().length }})
264
+ </button>
265
+ <button class="ogrid-header-filter__action-btn" (click)="handleClearSelection()">Clear</button>
266
+ </div>
267
+ <div class="ogrid-header-filter__options-list">
268
+ @if (isLoadingOptions()) {
269
+ <div class="ogrid-header-filter__loading">Loading...</div>
270
+ } @else if (filteredOptions().length === 0) {
271
+ <div class="ogrid-header-filter__empty">No options found</div>
272
+ } @else {
273
+ @for (option of filteredOptions(); track option) {
274
+ <label class="ogrid-header-filter__option">
275
+ <input
276
+ type="checkbox"
277
+ [checked]="tempSelected().has(option)"
278
+ (change)="handleCheckboxChange(option, $event)"
279
+ />
280
+ <span>{{ option }}</span>
281
+ </label>
282
+ }
283
+ }
284
+ </div>
285
+ <div class="ogrid-header-filter__popover-actions" style="border-top: 1px solid var(--ogrid-border, #e0e0e0);">
286
+ <button class="ogrid-header-filter__action-btn" [disabled]="tempSelected().size === 0" (click)="handleMultiSelectClear()">Clear</button>
287
+ <button class="ogrid-header-filter__action-btn ogrid-header-filter__action-btn--primary" (click)="handleMultiSelectApply()">Apply</button>
288
+ </div>
289
+ </div>
290
+ }
291
+ @case ('date') {
292
+ <div class="ogrid-header-filter__popover-body" style="width: 280px;">
293
+ <div style="padding: 12px;">
294
+ <div style="margin-bottom: 8px;">
295
+ <label style="display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600;">From</label>
296
+ <input
297
+ type="date"
298
+ class="ogrid-header-filter__input"
299
+ [value]="tempDateFrom()"
300
+ (change)="tempDateFrom.set(asInputValue($event))"
301
+ />
302
+ </div>
303
+ <div>
304
+ <label style="display: block; margin-bottom: 4px; font-size: 13px; font-weight: 600;">To</label>
305
+ <input
306
+ type="date"
307
+ class="ogrid-header-filter__input"
308
+ [value]="tempDateTo()"
309
+ (change)="tempDateTo.set(asInputValue($event))"
310
+ />
311
+ </div>
312
+ </div>
313
+ <div class="ogrid-header-filter__popover-actions">
314
+ <button class="ogrid-header-filter__action-btn" [disabled]="!tempDateFrom() && !tempDateTo()" (click)="handleDateClear()">Clear</button>
315
+ <button class="ogrid-header-filter__action-btn ogrid-header-filter__action-btn--primary" (click)="handleDateApply()">Apply</button>
316
+ </div>
317
+ </div>
318
+ }
319
+ }
320
+ </div>
321
+ }
322
+ `,
323
+ styles: [`
324
+ :host {
325
+ display: flex;
326
+ flex-direction: column;
327
+ height: 100%;
328
+ }
329
+ .ogrid-header-filter {
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: space-between;
333
+ gap: 4px;
334
+ height: 100%;
335
+ flex: 1;
336
+ }
337
+ .ogrid-header-filter__label {
338
+ flex: 1;
339
+ min-width: 0;
340
+ }
341
+ .ogrid-header-filter__name {
342
+ display: block;
343
+ overflow: hidden;
344
+ text-overflow: ellipsis;
345
+ white-space: nowrap;
346
+ }
347
+ .ogrid-header-filter__actions {
348
+ display: flex;
349
+ gap: 2px;
350
+ flex-shrink: 0;
351
+ }
352
+ .ogrid-header-filter__btn {
353
+ position: relative;
354
+ min-width: 20px;
355
+ height: 20px;
356
+ padding: 0;
357
+ border: none;
358
+ border-radius: 2px;
359
+ background: transparent;
360
+ color: var(--ogrid-fg, #242424);
361
+ cursor: pointer;
362
+ font-size: 12px;
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ opacity: 0.6;
367
+ transition: all 0.15s ease;
368
+ }
369
+ .ogrid-header-filter__btn:hover {
370
+ opacity: 1;
371
+ background: var(--ogrid-hover-bg, #f0f0f0);
372
+ }
373
+ .ogrid-header-filter__btn--active {
374
+ opacity: 1;
375
+ color: var(--ogrid-active-border, #0078d4);
376
+ font-weight: 700;
377
+ }
378
+ .ogrid-header-filter__dot {
379
+ position: absolute;
380
+ top: 2px;
381
+ right: 2px;
382
+ width: 4px;
383
+ height: 4px;
384
+ border-radius: 50%;
385
+ background: var(--ogrid-active-border, #0078d4);
386
+ }
387
+ .ogrid-header-filter__popover {
388
+ position: fixed;
389
+ z-index: 1000;
390
+ background: var(--ogrid-bg, #ffffff);
391
+ border: 1px solid var(--ogrid-border, #e0e0e0);
392
+ border-radius: 4px;
393
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15);
394
+ min-width: 200px;
395
+ }
396
+ .ogrid-header-filter__popover-header {
397
+ padding: 8px 12px;
398
+ font-size: 14px;
399
+ font-weight: 600;
400
+ color: var(--ogrid-fg, #242424);
401
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
402
+ background: var(--ogrid-header-bg, #f5f5f5);
403
+ }
404
+ .ogrid-header-filter__popover-body {
405
+ display: flex;
406
+ flex-direction: column;
407
+ }
408
+ .ogrid-header-filter__input {
409
+ width: 100%;
410
+ padding: 6px 8px;
411
+ border: 1px solid var(--ogrid-border, #e0e0e0);
412
+ border-radius: 4px;
413
+ font-size: 14px;
414
+ background: var(--ogrid-bg, #ffffff);
415
+ color: var(--ogrid-fg, #242424);
416
+ }
417
+ .ogrid-header-filter__input:focus {
418
+ outline: 2px solid var(--ogrid-active-border, #0078d4);
419
+ outline-offset: 1px;
420
+ }
421
+ .ogrid-header-filter__options-info {
422
+ margin-top: 6px;
423
+ font-size: 12px;
424
+ color: var(--ogrid-fg, #242424);
425
+ opacity: 0.7;
426
+ }
427
+ .ogrid-header-filter__select-actions {
428
+ display: flex;
429
+ gap: 8px;
430
+ padding: 8px 12px;
431
+ border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
432
+ }
433
+ .ogrid-header-filter__options-list {
434
+ max-height: 240px;
435
+ overflow-y: auto;
436
+ padding: 4px 0;
437
+ }
438
+ .ogrid-header-filter__option {
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 8px;
442
+ padding: 6px 12px;
443
+ cursor: pointer;
444
+ font-size: 14px;
445
+ color: var(--ogrid-fg, #242424);
446
+ transition: background 0.15s ease;
447
+ }
448
+ .ogrid-header-filter__option:hover {
449
+ background: var(--ogrid-hover-bg, #f0f0f0);
450
+ }
451
+ .ogrid-header-filter__loading,
452
+ .ogrid-header-filter__empty {
453
+ padding: 16px;
454
+ text-align: center;
455
+ font-size: 14px;
456
+ color: var(--ogrid-fg, #242424);
457
+ opacity: 0.7;
458
+ }
459
+ .ogrid-header-filter__popover-actions {
460
+ display: flex;
461
+ justify-content: flex-end;
462
+ gap: 8px;
463
+ padding: 8px 12px;
464
+ background: var(--ogrid-header-bg, #f5f5f5);
465
+ }
466
+ .ogrid-header-filter__action-btn {
467
+ padding: 6px 12px;
468
+ border: 1px solid var(--ogrid-border, #e0e0e0);
469
+ border-radius: 4px;
470
+ background: var(--ogrid-bg, #ffffff);
471
+ color: var(--ogrid-fg, #242424);
472
+ cursor: pointer;
473
+ font-size: 13px;
474
+ transition: all 0.15s ease;
475
+ }
476
+ .ogrid-header-filter__action-btn:hover:not(:disabled) {
477
+ background: var(--ogrid-hover-bg, #f0f0f0);
478
+ }
479
+ .ogrid-header-filter__action-btn:disabled {
480
+ opacity: 0.4;
481
+ cursor: not-allowed;
482
+ }
483
+ .ogrid-header-filter__action-btn--primary {
484
+ background: var(--ogrid-active-border, #0078d4);
485
+ color: #ffffff;
486
+ border-color: var(--ogrid-active-border, #0078d4);
487
+ }
488
+ .ogrid-header-filter__action-btn--primary:hover:not(:disabled) {
489
+ opacity: 0.9;
490
+ }
491
+ `],
492
+ host: {
493
+ '(document:click)': 'onDocumentClick($event)',
494
+ },
495
+ })
496
+ ], ColumnHeaderFilterComponent);
497
+ export { ColumnHeaderFilterComponent };