@oscarpalmer/tabela 0.9.0 → 0.11.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.
Files changed (60) hide show
  1. package/dist/components/body.component.js +3 -15
  2. package/dist/components/footer.component.js +3 -3
  3. package/dist/components/header.component.js +2 -2
  4. package/dist/components/row.component.js +19 -7
  5. package/dist/helpers/dom.helpers.js +5 -10
  6. package/dist/helpers/misc.helpers.js +7 -0
  7. package/dist/helpers/style.helper.js +6 -0
  8. package/dist/managers/column.manager.js +10 -9
  9. package/dist/managers/data.manager.js +26 -22
  10. package/dist/managers/event.manager.js +39 -19
  11. package/dist/managers/filter.manager.js +11 -10
  12. package/dist/managers/navigation.manager.js +73 -0
  13. package/dist/managers/render.manager.js +110 -0
  14. package/dist/managers/row.manager.js +7 -7
  15. package/dist/managers/selection.manager.js +189 -0
  16. package/dist/managers/sort.manager.js +9 -8
  17. package/dist/tabela.full.js +1096 -433
  18. package/dist/tabela.js +34 -11
  19. package/package.json +1 -1
  20. package/src/components/body.component.ts +5 -20
  21. package/src/components/footer.component.ts +3 -3
  22. package/src/components/header.component.ts +2 -2
  23. package/src/components/row.component.ts +30 -10
  24. package/src/helpers/dom.helpers.ts +3 -10
  25. package/src/helpers/misc.helpers.ts +15 -0
  26. package/src/helpers/style.helper.ts +6 -0
  27. package/src/managers/column.manager.ts +12 -14
  28. package/src/managers/data.manager.ts +31 -27
  29. package/src/managers/event.manager.ts +67 -37
  30. package/src/managers/filter.manager.ts +12 -11
  31. package/src/managers/navigation.manager.ts +145 -0
  32. package/src/managers/render.manager.ts +184 -0
  33. package/src/managers/row.manager.ts +9 -14
  34. package/src/managers/selection.manager.ts +332 -0
  35. package/src/managers/sort.manager.ts +14 -14
  36. package/src/models/render.model.ts +16 -0
  37. package/src/models/tabela.model.ts +23 -2
  38. package/src/tabela.ts +42 -10
  39. package/types/components/row.component.d.ts +4 -4
  40. package/types/helpers/dom.helpers.d.ts +1 -1
  41. package/types/helpers/misc.helpers.d.ts +2 -0
  42. package/types/helpers/style.helper.d.ts +1 -0
  43. package/types/managers/column.manager.d.ts +4 -5
  44. package/types/managers/data.manager.d.ts +5 -6
  45. package/types/managers/event.manager.d.ts +3 -6
  46. package/types/managers/filter.manager.d.ts +3 -3
  47. package/types/managers/navigation.manager.d.ts +10 -0
  48. package/types/managers/render.manager.d.ts +16 -0
  49. package/types/managers/row.manager.d.ts +4 -5
  50. package/types/managers/selection.manager.d.ts +18 -0
  51. package/types/managers/sort.manager.d.ts +4 -4
  52. package/types/models/render.model.d.ts +13 -0
  53. package/types/models/tabela.model.d.ts +21 -2
  54. package/types/tabela.d.ts +2 -1
  55. package/dist/managers/virtualization.manager.js +0 -101
  56. package/src/managers/virtualization.manager.ts +0 -176
  57. package/src/models/virtualization.model.ts +0 -14
  58. package/types/managers/virtualization.manager.d.ts +0 -18
  59. package/types/models/virtualization.model.d.ts +0 -12
  60. /package/dist/models/{virtualization.model.js → render.model.js} +0 -0
@@ -4,7 +4,7 @@ import {getString} from '@oscarpalmer/atoms/string';
4
4
  import {endsWith, includes, startsWith} from '@oscarpalmer/atoms/string/match';
5
5
  import {equal} from '@oscarpalmer/atoms/value/equal';
6
6
  import type {FilterComparison, FilterItem} from '../models/filter.model';
7
- import type {TabelaFilter, TabelaManagers} from '../models/tabela.model';
7
+ import type {TabelaFilter, TabelaState} from '../models/tabela.model';
8
8
 
9
9
  export class FilterManager {
10
10
  handlers = Object.freeze({
@@ -16,7 +16,7 @@ export class FilterManager {
16
16
 
17
17
  items: Record<string, FilterItem[]> = {};
18
18
 
19
- constructor(readonly managers: TabelaManagers) {}
19
+ constructor(public state: TabelaState) {}
20
20
 
21
21
  add(item: FilterItem): void {
22
22
  if (this.items[item.field] == null) {
@@ -44,20 +44,21 @@ export class FilterManager {
44
44
 
45
45
  destroy(): void {
46
46
  this.handlers = undefined as never;
47
- this.items = {};
47
+ this.items = undefined as never;
48
+ this.state = undefined as never;
48
49
  }
49
50
 
50
51
  filter(): void {
51
- const {managers} = this;
52
+ const {state} = this;
52
53
 
53
54
  const filtered: Key[] = [];
54
55
  const filters = Object.entries(this.items);
55
56
 
56
- const keysLength = managers.data.values.keys.original.length;
57
+ const keysLength = state.managers.data.values.keys.original.length;
57
58
 
58
59
  rowLoop: for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
59
- const key = managers.data.values.keys.original[keyIndex];
60
- const row = managers.data.values.objects.mapped.get(key);
60
+ const key = state.managers.data.values.keys.original[keyIndex];
61
+ const row = state.managers.data.values.objects.mapped.get(key);
61
62
 
62
63
  if (row == null) {
63
64
  continue;
@@ -82,12 +83,12 @@ export class FilterManager {
82
83
  filtered.push(key);
83
84
  }
84
85
 
85
- managers.data.values.keys.active = filtered;
86
+ state.managers.data.values.keys.active = filtered;
86
87
 
87
- if (managers.sort.items.length > 0) {
88
- managers.sort.sort();
88
+ if (state.managers.sort.items.length > 0) {
89
+ state.managers.sort.sort();
89
90
  } else {
90
- managers.virtualization.update(true, true);
91
+ state.managers.render.update(true, true);
91
92
  }
92
93
  }
93
94
 
@@ -0,0 +1,145 @@
1
+ import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
+ import type {Key} from '@oscarpalmer/atoms/models';
3
+ import {clamp} from '@oscarpalmer/atoms/number';
4
+ import {getKey} from '../helpers/misc.helpers';
5
+ import type {TabelaState} from '../models/tabela.model';
6
+
7
+ export class NavigationManager {
8
+ active: Key | undefined;
9
+
10
+ constructor(public state: TabelaState) {}
11
+
12
+ destroy(): void {
13
+ this.state = undefined as never;
14
+ }
15
+
16
+ handle(event: KeyboardEvent): void {
17
+ if (!allKeys.has(event.key)) {
18
+ return;
19
+ }
20
+
21
+ event.preventDefault();
22
+
23
+ const {components, id, managers} = this.state;
24
+
25
+ const activeDescendant = components.body.elements.group.getAttribute('aria-activedescendant');
26
+
27
+ const keys = managers.data.values.keys.active ?? managers.data.values.keys.original;
28
+ const {length} = keys;
29
+
30
+ let next: number;
31
+
32
+ if (isNullableOrWhitespace(activeDescendant)) {
33
+ next = getDefaultIndex(event.key, length);
34
+ } else {
35
+ next = getIndex(event, activeDescendant, id, keys)!;
36
+ }
37
+
38
+ if (next != null) {
39
+ this.setActive(keys.at(next)!);
40
+ }
41
+ }
42
+
43
+ setActive(key: Key | undefined, scroll?: boolean): void {
44
+ const {components, managers, options} = this.state;
45
+
46
+ this.active = key;
47
+
48
+ const active = components.body.elements.group.querySelectorAll('[data-active="true"]');
49
+
50
+ for (const item of active) {
51
+ item.setAttribute('data-active', 'false');
52
+ }
53
+
54
+ const row = managers.row.get(key!);
55
+
56
+ if (row != null) {
57
+ row.element?.setAttribute('data-active', 'true');
58
+
59
+ if (scroll ?? true) {
60
+ if (row.element == null) {
61
+ components.body.elements.group.scrollTo({
62
+ top: managers.data.getIndex(key!) * options.rowHeight,
63
+ behavior: 'smooth',
64
+ });
65
+ } else {
66
+ row.element.scrollIntoView({
67
+ block: 'nearest',
68
+ });
69
+ }
70
+ }
71
+ }
72
+
73
+ components.body.elements.group.setAttribute(
74
+ 'aria-activedescendant',
75
+ row == null ? '' : `tabela_${this.state.id}_row_${key}`,
76
+ );
77
+ }
78
+ }
79
+
80
+ function getDefaultIndex(key: string, max: number): number {
81
+ switch (true) {
82
+ case negativeDefaultKeys.has(key):
83
+ return -1;
84
+
85
+ case key === 'PageDown':
86
+ return Math.min(9, max - 1);
87
+
88
+ case key === 'PageUp':
89
+ return max < 10 ? 0 : max - 10;
90
+
91
+ default:
92
+ return 0;
93
+ }
94
+ }
95
+
96
+ function getIndex(
97
+ event: KeyboardEvent,
98
+ active: string,
99
+ id: number,
100
+ keys: Key[],
101
+ ): number | undefined {
102
+ const key = getKey(active.replace(`tabela_${id}_row_`, ''));
103
+
104
+ if (key == null) {
105
+ return;
106
+ }
107
+
108
+ if (absoluteKeys.has(event.key)) {
109
+ return event.key === 'Home' ? 0 : keys.length - 1;
110
+ }
111
+
112
+ const index = keys.indexOf(key);
113
+ const offset = getOffset(event.key);
114
+
115
+ return clamp(index + offset, 0, keys.length - 1, true);
116
+ }
117
+
118
+ function getOffset(key: string): number {
119
+ switch (key) {
120
+ case 'ArrowDown':
121
+ return 1;
122
+
123
+ case 'ArrowUp':
124
+ return -1;
125
+
126
+ case 'PageDown':
127
+ return 10;
128
+
129
+ case 'PageUp':
130
+ return -10;
131
+
132
+ default:
133
+ return 0;
134
+ }
135
+ }
136
+
137
+ const absoluteKeys = new Set(['End', 'Home']);
138
+
139
+ const arrowKeys = new Set(['ArrowDown', 'ArrowUp']);
140
+
141
+ const negativeDefaultKeys = new Set(['ArrowUp', 'End']);
142
+
143
+ const pageKeys = new Set(['PageDown', 'PageUp']);
144
+
145
+ const allKeys = new Set([...absoluteKeys, ...arrowKeys, ...pageKeys]);
@@ -0,0 +1,184 @@
1
+ import type {Key} from '@oscarpalmer/atoms/models';
2
+ import {on} from '@oscarpalmer/toretto/event';
3
+ import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
4
+ import {removeRow, renderRow} from '../components/row.component';
5
+ import type {RenderElementPool, RenderRange, RenderState} from '../models/render.model';
6
+ import type {TabelaState} from '../models/tabela.model';
7
+
8
+ function getRange(this: RenderManager, down: boolean): RenderRange {
9
+ const {components, managers, options} = this.state;
10
+ const {clientHeight, scrollTop} = components.body.elements.group;
11
+
12
+ const first = Math.floor(scrollTop / options.rowHeight);
13
+
14
+ const last = Math.min(
15
+ (managers.data.values.keys.active?.length ?? managers.data.values.keys.original.length) - 1,
16
+ Math.ceil((scrollTop + clientHeight) / options.rowHeight) - 1,
17
+ );
18
+
19
+ const before = Math.ceil(clientHeight / options.rowHeight) * (down ? 1 : 2);
20
+ const after = Math.ceil(clientHeight / options.rowHeight) * (down ? 2 : 1);
21
+
22
+ const start = Math.max(0, first - before);
23
+
24
+ const end = Math.min(
25
+ (managers.data.values.keys.active?.length ?? managers.data.values.keys.original.length) - 1,
26
+ last + after,
27
+ );
28
+
29
+ return {end, start};
30
+ }
31
+
32
+ function onScroll(this: RenderManager): void {
33
+ const {state} = this;
34
+
35
+ if (!state.active) {
36
+ requestAnimationFrame(() => {
37
+ const top = state.components.body.elements.group.scrollTop;
38
+
39
+ this.update(top > state.top);
40
+
41
+ state.active = false;
42
+ state.top = top;
43
+ });
44
+
45
+ state.active = true;
46
+ }
47
+ }
48
+
49
+ export class RenderManager {
50
+ fragment!: DocumentFragment;
51
+
52
+ listener: RemovableEventListener;
53
+
54
+ pool: RenderElementPool = {
55
+ cells: {},
56
+ rows: [],
57
+ };
58
+
59
+ state: RenderState;
60
+
61
+ visible = new Map<number, Key>();
62
+
63
+ constructor(state: TabelaState) {
64
+ this.listener = on(state.components.body.elements.group, 'scroll', onScroll.bind(this));
65
+
66
+ this.state = {
67
+ ...state,
68
+ active: false,
69
+ top: 0,
70
+ };
71
+ }
72
+
73
+ destroy(): void {
74
+ const {listener, pool, visible} = this;
75
+
76
+ listener();
77
+ visible.clear();
78
+
79
+ pool.cells = {};
80
+ pool.rows = [];
81
+
82
+ this.fragment = undefined as never;
83
+ this.listener = undefined as never;
84
+ this.pool = undefined as never;
85
+ this.state = undefined as never;
86
+ this.visible = undefined as never;
87
+ }
88
+
89
+ removeCells(fields: string[]): void {
90
+ const {pool, state, visible} = this;
91
+ const {length} = fields;
92
+
93
+ for (let index = 0; index < length; index += 1) {
94
+ delete pool.cells[fields[index]];
95
+ }
96
+
97
+ for (const [, key] of visible) {
98
+ const row = state.managers.row.get(key);
99
+
100
+ if (row == null || row.element == null) {
101
+ continue;
102
+ }
103
+
104
+ for (let index = 0; index < length; index += 1) {
105
+ row.cells[fields[index]].innerHTML = '';
106
+
107
+ row.cells[fields[index]].remove();
108
+
109
+ delete row.cells[fields[index]];
110
+ }
111
+ }
112
+ }
113
+
114
+ getFragment(): DocumentFragment {
115
+ this.fragment ??= document.createDocumentFragment();
116
+
117
+ this.fragment.replaceChildren();
118
+
119
+ return this.fragment;
120
+ }
121
+
122
+ update(down: boolean, rerender?: boolean): void {
123
+ const {state, pool, visible} = this;
124
+ const {components, managers, options} = state;
125
+
126
+ components.body.elements.faker.style.height = `${managers.data.size * options.rowHeight}px`;
127
+
128
+ const indices = new Set<number>();
129
+ const range = getRange.call(this, down);
130
+
131
+ for (let index = range.start; index <= range.end; index += 1) {
132
+ indices.add(index);
133
+ }
134
+
135
+ let remove = rerender ?? false;
136
+
137
+ for (const [index, key] of visible) {
138
+ const row = managers.row.get(key);
139
+
140
+ if (remove || row == null || !indices.has(index)) {
141
+ visible.delete(index);
142
+
143
+ if (row != null) {
144
+ removeRow(pool, row);
145
+ }
146
+ }
147
+ }
148
+
149
+ const fragment = this.getFragment();
150
+
151
+ const keys = managers.data.values.keys.active ?? managers.data.values.keys.original;
152
+
153
+ let count = 0;
154
+
155
+ for (let index = range.start; index <= range.end; index += 1) {
156
+ if (visible.has(index)) {
157
+ continue;
158
+ }
159
+
160
+ const key = keys[index];
161
+ const row = managers.row.get(key);
162
+
163
+ if (row == null) {
164
+ continue;
165
+ }
166
+
167
+ count += 1;
168
+
169
+ renderRow(state, row);
170
+
171
+ visible.set(index, key);
172
+
173
+ if (row.element != null) {
174
+ row.element.style.transform = `translateY(${index * options.rowHeight}px)`;
175
+
176
+ fragment.append(row.element);
177
+ }
178
+ }
179
+
180
+ if (count > 0) {
181
+ components.body.elements.group[down ? 'append' : 'prepend'](fragment);
182
+ }
183
+ }
184
+ }
@@ -1,29 +1,24 @@
1
1
  import type {Key} from '@oscarpalmer/atoms/models';
2
2
  import {removeRow, renderRow, RowComponent} from '../components/row.component';
3
- import type {TabelaManagers} from '../models/tabela.model';
3
+ import type {TabelaState} from '../models/tabela.model';
4
4
 
5
5
  export class RowManager {
6
- readonly components: Map<Key, RowComponent> = new Map();
6
+ components = new Map<Key, RowComponent>();
7
7
 
8
- readonly height: number;
9
-
10
- constructor(
11
- readonly managers: TabelaManagers,
12
- rowHeight: number,
13
- ) {
14
- this.height = rowHeight;
15
- }
8
+ constructor(public state: TabelaState) {}
16
9
 
17
10
  destroy(): void {
18
11
  const components = [...this.components.values()];
19
-
20
12
  const {length} = components;
21
13
 
22
14
  for (let index = 0; index < length; index += 1) {
23
- removeRow(this.managers.virtualization.pool, components[index]);
15
+ removeRow(this.state.managers.render.pool, components[index]);
24
16
  }
25
17
 
26
18
  this.components.clear();
19
+
20
+ this.components = undefined as never;
21
+ this.state = undefined as never;
27
22
  }
28
23
 
29
24
  get(key: Key): RowComponent | undefined {
@@ -46,7 +41,7 @@ export class RowManager {
46
41
  const row = this.components.get(key);
47
42
 
48
43
  if (row != null) {
49
- removeRow(this.managers.virtualization.pool, row);
44
+ removeRow(this.state.managers.render.pool, row);
50
45
 
51
46
  this.components.delete(key);
52
47
  }
@@ -56,7 +51,7 @@ export class RowManager {
56
51
  const row = this.components.get(key);
57
52
 
58
53
  if (row != null) {
59
- renderRow(this.managers, row);
54
+ renderRow(this.state, row);
60
55
  }
61
56
  }
62
57
  }