@oscarpalmer/tabela 0.12.0 → 0.14.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 (65) hide show
  1. package/dist/tabela.full.js +146 -18
  2. package/package.json +45 -37
  3. package/src/components/body.component.ts +11 -7
  4. package/src/components/column.component.ts +23 -24
  5. package/src/components/footer.component.ts +7 -5
  6. package/src/components/group.component.ts +73 -9
  7. package/src/components/header.component.ts +6 -4
  8. package/src/components/row.component.ts +28 -18
  9. package/src/helpers/dom.helpers.ts +27 -29
  10. package/src/helpers/misc.helpers.ts +5 -0
  11. package/src/helpers/style.helper.ts +1 -1
  12. package/src/managers/column.manager.ts +4 -0
  13. package/src/managers/data.manager.ts +197 -124
  14. package/src/managers/event.manager.ts +27 -17
  15. package/src/managers/filter.manager.ts +49 -25
  16. package/src/managers/group.manager.ts +73 -12
  17. package/src/managers/navigation.manager.ts +48 -50
  18. package/src/managers/render.manager.ts +56 -29
  19. package/src/managers/row.manager.ts +22 -10
  20. package/src/managers/selection.manager.ts +40 -31
  21. package/src/managers/sort.manager.ts +58 -43
  22. package/src/managers/style.manager.ts +171 -0
  23. package/src/models/column.model.ts +2 -6
  24. package/src/models/data.model.ts +12 -10
  25. package/src/models/dom.model.ts +33 -0
  26. package/src/models/event.model.ts +7 -0
  27. package/src/models/filter.model.ts +20 -0
  28. package/src/models/group.model.ts +10 -2
  29. package/src/models/sort.model.ts +4 -0
  30. package/src/models/style.model.ts +51 -0
  31. package/src/models/tabela.model.ts +11 -8
  32. package/src/tabela.ts +67 -37
  33. package/types/components/body.component.d.ts +0 -6
  34. package/types/components/column.component.d.ts +0 -13
  35. package/types/components/footer.component.d.ts +0 -8
  36. package/types/components/group.component.d.ts +0 -14
  37. package/types/components/header.component.d.ts +0 -8
  38. package/types/components/row.component.d.ts +0 -11
  39. package/types/helpers/dom.helpers.d.ts +0 -10
  40. package/types/helpers/misc.helpers.d.ts +0 -2
  41. package/types/helpers/style.helper.d.ts +0 -1
  42. package/types/index.d.ts +0 -4
  43. package/types/managers/column.manager.d.ts +0 -12
  44. package/types/managers/data.manager.d.ts +0 -29
  45. package/types/managers/event.manager.d.ts +0 -7
  46. package/types/managers/filter.manager.d.ts +0 -19
  47. package/types/managers/group.manager.d.ts +0 -17
  48. package/types/managers/navigation.manager.d.ts +0 -10
  49. package/types/managers/render.manager.d.ts +0 -17
  50. package/types/managers/row.manager.d.ts +0 -13
  51. package/types/managers/selection.manager.d.ts +0 -24
  52. package/types/managers/sort.manager.d.ts +0 -28
  53. package/types/models/body.model.d.ts +0 -4
  54. package/types/models/column.model.d.ts +0 -13
  55. package/types/models/data.model.d.ts +0 -24
  56. package/types/models/filter.model.d.ts +0 -13
  57. package/types/models/footer.model.d.ts +0 -5
  58. package/types/models/group.model.d.ts +0 -4
  59. package/types/models/header.model.d.ts +0 -4
  60. package/types/models/render.model.d.ts +0 -13
  61. package/types/models/selection.model.d.ts +0 -8
  62. package/types/models/sort.model.d.ts +0 -12
  63. package/types/models/tabela.model.d.ts +0 -39
  64. package/types/models/tabela.options.d.ts +0 -10
  65. package/types/tabela.d.ts +0 -15
@@ -3,17 +3,30 @@ import {getNumber} from '@oscarpalmer/atoms/number';
3
3
  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
- import type {TabelaFilter, TabelaFilterComparison, TabelaFilterItem} from '../models/filter.model';
6
+ import {isGroupKey} from '../helpers/misc.helpers';
7
+ import {
8
+ FILTER_CONTAINS,
9
+ FILTER_ENDS_WITH,
10
+ FILTER_EQUALS,
11
+ FILTER_GREATER_THAN,
12
+ FILTER_GREATER_THAN_OR_EQUAL,
13
+ FILTER_LESS_THAN,
14
+ FILTER_LESS_THAN_OR_EQUAL,
15
+ FILTER_NOT_CONTAINS,
16
+ FILTER_NOT_EQUALS,
17
+ FILTER_STARTS_WITH,
18
+ type TabelaFilter,
19
+ type TabelaFilterItem,
20
+ } from '../models/filter.model';
7
21
  import type {State} from '../models/tabela.model';
8
- import {GroupComponent} from '../components/group.component';
9
22
 
10
23
  export class FilterManager {
11
- handlers = Object.freeze({
24
+ handlers: TabelaFilter = {
12
25
  add: item => this.add(item),
13
26
  clear: () => this.clear(),
14
27
  remove: value => this.remove(value),
15
28
  set: items => this.set(items),
16
- } satisfies TabelaFilter);
29
+ };
17
30
 
18
31
  items: Record<string, TabelaFilterItem[]> = {};
19
32
 
@@ -25,7 +38,7 @@ export class FilterManager {
25
38
  } else {
26
39
  const index = this.items[item.field].findIndex(existing => equal(existing, item));
27
40
 
28
- if (index !== -1) {
41
+ if (index > -1) {
29
42
  return;
30
43
  }
31
44
  }
@@ -52,21 +65,21 @@ export class FilterManager {
52
65
  filter(): void {
53
66
  const {state} = this;
54
67
 
55
- const filtered: Array<GroupComponent | Key> = [];
68
+ const filtered: Key[] = [];
56
69
  const filters = Object.entries(this.items);
57
70
 
58
- const keysLength = state.managers.data.values.keys.original.length;
71
+ const itemsLength = state.managers.data.state.keys.original.length;
59
72
 
60
- rowLoop: for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
61
- const key = state.managers.data.values.keys.original[keyIndex];
73
+ rowLoop: for (let itemIndex = 0; itemIndex < itemsLength; itemIndex += 1) {
74
+ const item = state.managers.data.state.keys.original[itemIndex];
62
75
 
63
- if (key instanceof GroupComponent) {
64
- filtered.push(key);
76
+ if (isGroupKey(item)) {
77
+ filtered.push(item);
65
78
 
66
79
  continue;
67
80
  }
68
81
 
69
- const row = state.managers.data.values.objects.mapped.get(key);
82
+ const row = state.managers.data.state.values.mapped.get(item);
70
83
 
71
84
  if (row == null) {
72
85
  continue;
@@ -88,10 +101,10 @@ export class FilterManager {
88
101
  continue rowLoop;
89
102
  }
90
103
 
91
- filtered.push(key);
104
+ filtered.push(item);
92
105
  }
93
106
 
94
- state.managers.data.values.keys.active = filtered;
107
+ state.managers.data.state.keys.active = filtered;
95
108
 
96
109
  if (state.managers.sort.items.length > 0) {
97
110
  state.managers.sort.sort();
@@ -108,6 +121,17 @@ export class FilterManager {
108
121
 
109
122
  const keyed: Record<string, TabelaFilterItem[]> = {};
110
123
 
124
+ const filters = Object.keys(this.items);
125
+ const {length} = filters;
126
+
127
+ for (let index = 0; index < length; index += 1) {
128
+ const field = filters[index];
129
+
130
+ if (field !== value) {
131
+ keyed[field] = this.items[field];
132
+ }
133
+ }
134
+
111
135
  this.items = keyed;
112
136
  } else {
113
137
  const {field} = value;
@@ -145,17 +169,17 @@ export class FilterManager {
145
169
  }
146
170
  }
147
171
 
148
- const comparators: Record<TabelaFilterComparison, (row: unknown, filter: unknown) => boolean> = {
149
- contains: (row, filter) => includes(getString(row), getString(filter), true),
150
- 'ends-with': (row, filter) => endsWith(getString(row), getString(filter), true),
151
- equals: (row, filter) => equalizer(row, filter),
152
- 'greater-than': (row, filter) => getNumber(row) > getNumber(filter),
153
- 'greater-than-or-equal': (row, filter) => getNumber(row) >= getNumber(filter),
154
- 'less-than': (row, filter) => getNumber(row) < getNumber(filter),
155
- 'less-than-or-equal': (row, filter) => getNumber(row) <= getNumber(filter),
156
- 'not-contains': (row, filter) => !includes(getString(row), getString(filter), true),
157
- 'not-equals': (row, filter) => !equalizer(row, filter),
158
- 'starts-with': (row, filter) => startsWith(getString(row), getString(filter), true),
172
+ const comparators: Record<string, (row: unknown, filter: unknown) => boolean> = {
173
+ [FILTER_CONTAINS]: (row, filter) => includes(getString(row), getString(filter), true),
174
+ [FILTER_ENDS_WITH]: (row, filter) => endsWith(getString(row), getString(filter), true),
175
+ [FILTER_EQUALS]: (row, filter) => equalizer(row, filter),
176
+ [FILTER_GREATER_THAN]: (row, filter) => getNumber(row) > getNumber(filter),
177
+ [FILTER_GREATER_THAN_OR_EQUAL]: (row, filter) => getNumber(row) >= getNumber(filter),
178
+ [FILTER_LESS_THAN]: (row, filter) => getNumber(row) < getNumber(filter),
179
+ [FILTER_LESS_THAN_OR_EQUAL]: (row, filter) => getNumber(row) <= getNumber(filter),
180
+ [FILTER_NOT_CONTAINS]: (row, filter) => !includes(getString(row), getString(filter), true),
181
+ [FILTER_NOT_EQUALS]: (row, filter) => !equalizer(row, filter),
182
+ [FILTER_STARTS_WITH]: (row, filter) => startsWith(getString(row), getString(filter), true),
159
183
  };
160
184
 
161
185
  const equalizer = equal.initialize({
@@ -1,8 +1,11 @@
1
1
  import {sort} from '@oscarpalmer/atoms/array';
2
+ import {toMap} from '@oscarpalmer/atoms/array/to-map';
2
3
  import {toRecord} from '@oscarpalmer/atoms/array/to-record';
3
4
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
4
5
  import type {Key, Simplify} from '@oscarpalmer/atoms/models';
5
- import type {GroupComponent} from '../components/group.component';
6
+ import {getString} from '@oscarpalmer/atoms/string';
7
+ import {removeGroup, type GroupComponent} from '../components/group.component';
8
+ import type {TabelaGroup} from '../models/group.model';
6
9
  import type {State} from '../models/tabela.model';
7
10
 
8
11
  export class GroupManager {
@@ -12,11 +15,26 @@ export class GroupManager {
12
15
 
13
16
  field!: string;
14
17
 
18
+ handlers: TabelaGroup = {
19
+ set: (group?: string) => {
20
+ if (group === this.field) {
21
+ return;
22
+ }
23
+
24
+ this.enabled = !isNullableOrWhitespace(group);
25
+ this.field = group ?? '';
26
+
27
+ this.state.managers.data.set(this.state.managers.data.get());
28
+ },
29
+ };
30
+
15
31
  items: GroupComponent[] = [];
16
32
 
33
+ mapped = new Map<string, GroupComponent>();
34
+
17
35
  order: Record<never, number> = {};
18
36
 
19
- constructor(readonly state: State) {
37
+ constructor(public state: State) {
20
38
  if (isNullableOrWhitespace(state.options.grouping)) {
21
39
  return;
22
40
  }
@@ -29,13 +47,42 @@ export class GroupManager {
29
47
  this.set([...this.items, group]);
30
48
  }
31
49
 
32
- get(value: unknown) {
33
- return this.items.find(item => item.value === value);
50
+ clear(): void {
51
+ const groups = this.items.splice(0);
52
+ const {length} = groups;
53
+
54
+ for (let index = 0; index < length; index += 1) {
55
+ this.remove(groups[index]);
56
+ }
57
+ }
58
+
59
+ destroy(): void {
60
+ const groups = this.items.splice(0);
61
+ const {length} = groups;
62
+
63
+ for (let index = 0; index < length; index += 1) {
64
+ removeGroup(groups[index]);
65
+ }
66
+
67
+ this.collapsed.clear();
68
+
69
+ this.handlers = undefined as never;
70
+ this.state = undefined as never;
71
+ }
72
+
73
+ getForKey(key: string): GroupComponent | undefined {
74
+ return this.mapped.get(key);
75
+ }
76
+
77
+ getForValue(value: unknown): GroupComponent | undefined {
78
+ const asString = getString(value);
79
+
80
+ return this.items.find(item => item.value.stringified === asString);
34
81
  }
35
82
 
36
83
  handle(button: HTMLElement): void {
37
- const key = button.dataset.key;
38
- const group = this.get(key);
84
+ const key = button.dataset.key?.replace(`${this.state.prefix}_`, '');
85
+ const group = this.getForKey(key ?? '');
39
86
 
40
87
  if (group == null) {
41
88
  return;
@@ -47,15 +94,15 @@ export class GroupManager {
47
94
 
48
95
  const index = items.indexOf(group);
49
96
 
50
- let first = state.managers.data.values.keys.original.indexOf(items[index]) + 1;
97
+ let first = state.managers.data.state.keys.original.indexOf(group.key) + 1;
51
98
 
52
99
  const last =
53
100
  items[index + 1] == null
54
- ? state.managers.data.keys.length - 1
55
- : state.managers.data.values.keys.original.indexOf(items[index + 1]) - 1;
101
+ ? state.managers.data.state.keys.original.length - 1
102
+ : state.managers.data.state.keys.original.indexOf(items[index + 1].key) - 1;
56
103
 
57
104
  for (; first <= last; first += 1) {
58
- const key = state.managers.data.values.keys.original[first] as Key;
105
+ const key = state.managers.data.state.keys.original[first] as Key;
59
106
 
60
107
  if (group.expanded) {
61
108
  collapsed.delete(key);
@@ -64,16 +111,30 @@ export class GroupManager {
64
111
  }
65
112
  }
66
113
 
67
- state.managers.render.update(true, true);
114
+ if (Object.keys(state.managers.filter.items).length > 0) {
115
+ state.managers.filter.filter();
116
+ } else if (state.managers.sort.items.length > 0) {
117
+ state.managers.sort.sort();
118
+ } else {
119
+ state.managers.render.update(true, true);
120
+ }
68
121
  }
69
122
 
70
123
  remove(group: GroupComponent): void {
124
+ removeGroup(group);
125
+
71
126
  this.set(this.items.filter(item => item !== group));
72
127
  }
73
128
 
74
129
  set(items: GroupComponent[]) {
75
130
  this.items = sort(items, item => item.label);
76
131
 
77
- this.order = toRecord(items as Simplify<GroupComponent>[], 'value', (_, index) => index);
132
+ this.mapped = toMap(items, group => group.key);
133
+
134
+ this.order = toRecord(
135
+ items as Simplify<GroupComponent>[],
136
+ group => group.value.stringified,
137
+ (_, index) => index,
138
+ );
78
139
  }
79
140
  }
@@ -1,8 +1,8 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
2
  import type {Key} from '@oscarpalmer/atoms/models';
3
3
  import {clamp} from '@oscarpalmer/atoms/number';
4
- import type {GroupComponent} from '../components/group.component';
5
- import {getKey} from '../helpers/misc.helpers';
4
+ import {getKey, isGroupKey} from '../helpers/misc.helpers';
5
+ import {ARIA_ACTIVEDESCENDANT, ATTRIBUTE_DATA_ACTIVE} from '../models/dom.model';
6
6
  import type {State} from '../models/tabela.model';
7
7
 
8
8
  export class NavigationManager {
@@ -21,9 +21,9 @@ export class NavigationManager {
21
21
 
22
22
  event.preventDefault();
23
23
 
24
- const {components, id, managers} = this.state;
24
+ const {components, managers} = this.state;
25
25
 
26
- const activeDescendant = components.body.elements.group.getAttribute('aria-activedescendant');
26
+ const activeDescendant = components.body.elements.group.getAttribute(ARIA_ACTIVEDESCENDANT);
27
27
 
28
28
  const {keys} = managers.data;
29
29
  const {length} = keys;
@@ -33,38 +33,40 @@ export class NavigationManager {
33
33
  if (isNullableOrWhitespace(activeDescendant)) {
34
34
  next = getDefaultIndex(event.key, length);
35
35
  } else {
36
- next = getIndex(event, activeDescendant, id, keys)!;
36
+ next = getIndex(this.state, event, activeDescendant)!;
37
37
  }
38
38
 
39
39
  if (next != null) {
40
- this.setActive(keys.at(next) as Key);
40
+ this.setActive(keys.at(next));
41
41
  }
42
42
  }
43
43
 
44
- setActive(key: Key | undefined, scroll?: boolean): void {
45
- const {components, managers, options} = this.state;
44
+ setActive(item: Key | undefined, scroll?: boolean): void {
45
+ const {components, managers, options, prefix} = this.state;
46
46
 
47
- this.active = key;
47
+ this.active = item;
48
48
 
49
- const active = components.body.elements.group.querySelectorAll('[data-active="true"]');
49
+ const active = components.body.elements.group.querySelectorAll(attributeDataActiveTrue);
50
50
 
51
51
  for (const item of active) {
52
- item.setAttribute('data-active', 'false');
52
+ item.setAttribute(ATTRIBUTE_DATA_ACTIVE, 'false');
53
53
  }
54
54
 
55
- const row = managers.row.get(key!);
55
+ const component = isGroupKey(item)
56
+ ? managers.group.getForKey(item as string)
57
+ : managers.row.get(item!, false);
56
58
 
57
- if (row != null) {
58
- row.element?.setAttribute('data-active', 'true');
59
+ if (component != null) {
60
+ component.element?.setAttribute(ATTRIBUTE_DATA_ACTIVE, 'true');
59
61
 
60
62
  if (scroll ?? true) {
61
- if (row.element == null) {
63
+ if (component.element == null) {
62
64
  components.body.elements.group.scrollTo({
63
- top: managers.data.getIndex(key!) * options.rowHeight,
65
+ top: managers.data.getIndex(item!) * options.rowHeight,
64
66
  behavior: 'smooth',
65
67
  });
66
68
  } else {
67
- row.element.scrollIntoView({
69
+ component.element.scrollIntoView({
68
70
  block: 'nearest',
69
71
  });
70
72
  }
@@ -72,8 +74,8 @@ export class NavigationManager {
72
74
  }
73
75
 
74
76
  components.body.elements.group.setAttribute(
75
- 'aria-activedescendant',
76
- row == null ? '' : `tabela_${this.state.id}_row_${key}`,
77
+ ARIA_ACTIVEDESCENDANT,
78
+ component == null ? '' : `${prefix}${component.key}`,
77
79
  );
78
80
  }
79
81
  }
@@ -83,10 +85,10 @@ function getDefaultIndex(key: string, max: number): number {
83
85
  case negativeDefaultKeys.has(key):
84
86
  return -1;
85
87
 
86
- case key === 'PageDown':
88
+ case key === KEY_PAGE_DOWN:
87
89
  return Math.min(9, max - 1);
88
90
 
89
- case key === 'PageUp':
91
+ case key === KEY_PAGE_UP:
90
92
  return max < 10 ? 0 : max - 10;
91
93
 
92
94
  default:
@@ -94,53 +96,49 @@ function getDefaultIndex(key: string, max: number): number {
94
96
  }
95
97
  }
96
98
 
97
- function getIndex(
98
- event: KeyboardEvent,
99
- active: string,
100
- id: number,
101
- keys: Array<GroupComponent | Key>,
102
- ): number | undefined {
103
- const key = getKey(active.replace(`tabela_${id}_row_`, ''));
99
+ function getIndex(state: State, event: KeyboardEvent, active: string): number | undefined {
100
+ const key = getKey(active.replace(state.prefix, ''));
104
101
 
105
102
  if (key == null) {
106
103
  return;
107
104
  }
108
105
 
109
106
  if (absoluteKeys.has(event.key)) {
110
- return event.key === 'Home' ? 0 : keys.length - 1;
107
+ return event.key === KEY_HOME ? 0 : state.managers.data.size - 1;
111
108
  }
112
109
 
113
- const index = keys.indexOf(key);
114
- const offset = getOffset(event.key);
110
+ const index = state.managers.data.getIndex(key);
115
111
 
116
- return clamp(index + offset, 0, keys.length - 1, true);
112
+ return clamp(index + (offset[event.key] ?? 0), 0, state.managers.data.size - 1, true);
117
113
  }
118
114
 
119
- function getOffset(key: string): number {
120
- switch (key) {
121
- case 'ArrowDown':
122
- return 1;
115
+ const KEY_ARROW_DOWN = 'ArrowDown';
123
116
 
124
- case 'ArrowUp':
125
- return -1;
117
+ const KEY_ARROW_UP = 'ArrowUp';
126
118
 
127
- case 'PageDown':
128
- return 10;
119
+ const KEY_END = 'End';
129
120
 
130
- case 'PageUp':
131
- return -10;
121
+ const KEY_HOME = 'Home';
132
122
 
133
- default:
134
- return 0;
135
- }
136
- }
123
+ const KEY_PAGE_DOWN = 'PageDown';
124
+
125
+ const KEY_PAGE_UP = 'PageUp';
126
+
127
+ const absoluteKeys = new Set([KEY_END, KEY_HOME]);
128
+
129
+ const arrowKeys = new Set([KEY_ARROW_DOWN, KEY_ARROW_UP]);
137
130
 
138
- const absoluteKeys = new Set(['End', 'Home']);
131
+ export const attributeDataActiveTrue = `[${ATTRIBUTE_DATA_ACTIVE}="true"]`;
139
132
 
140
- const arrowKeys = new Set(['ArrowDown', 'ArrowUp']);
133
+ const negativeDefaultKeys = new Set([KEY_ARROW_UP, KEY_END]);
141
134
 
142
- const negativeDefaultKeys = new Set(['ArrowUp', 'End']);
135
+ const offset: Record<string, number> = {
136
+ [KEY_ARROW_DOWN]: 1,
137
+ [KEY_ARROW_UP]: -1,
138
+ [KEY_PAGE_DOWN]: 10,
139
+ [KEY_PAGE_UP]: -10,
140
+ };
143
141
 
144
- const pageKeys = new Set(['PageDown', 'PageUp']);
142
+ const pageKeys = new Set([KEY_PAGE_DOWN, KEY_PAGE_UP]);
145
143
 
146
144
  const allKeys = new Set([...absoluteKeys, ...arrowKeys, ...pageKeys]);
@@ -1,29 +1,30 @@
1
1
  import type {Key} from '@oscarpalmer/atoms/models';
2
2
  import {on} from '@oscarpalmer/toretto/event';
3
3
  import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
4
- import {GroupComponent, renderGroup} from '../components/group.component';
4
+ import {renderGroup} from '../components/group.component';
5
5
  import {removeRow, renderRow} from '../components/row.component';
6
+ import {isGroupKey} from '../helpers/misc.helpers';
6
7
  import type {RenderElementPool, RenderRange, RenderState} from '../models/render.model';
7
8
  import type {State} from '../models/tabela.model';
8
9
 
9
10
  function getRange(state: State, down: boolean): RenderRange {
10
- const {components, managers, options} = state;
11
- const {clientHeight, scrollTop} = components.body.elements.group;
11
+ const {element, managers, options} = state;
12
+ const {clientHeight, scrollTop} = element;
12
13
 
13
14
  const {keys} = managers.data;
14
15
 
15
- const first = Math.floor(scrollTop / options.rowHeight);
16
+ const firstIndex = Math.floor(scrollTop / options.rowHeight);
17
+ const lastIndex = keys.length - managers.group.collapsed.size - 1;
16
18
 
17
- const last = Math.min(
18
- keys.length - managers.group.collapsed.size - 1,
19
- Math.ceil((scrollTop + clientHeight) / options.rowHeight) - 1,
20
- );
19
+ const last = Math.min(lastIndex, Math.ceil((scrollTop + clientHeight) / options.rowHeight) - 1);
21
20
 
22
- const before = Math.ceil(clientHeight / options.rowHeight) * (down ? 1 : 2);
23
- const after = Math.ceil(clientHeight / options.rowHeight) * (down ? 2 : 1);
21
+ const visible = clientHeight / options.rowHeight;
24
22
 
25
- const start = Math.max(0, first - before);
26
- const end = Math.min(keys.length - managers.group.collapsed.size - 1, last + after);
23
+ const before = Math.ceil(visible) * (down ? 1 : 2);
24
+ const after = Math.ceil(visible) * (down ? 2 : 1);
25
+
26
+ const start = Math.max(0, firstIndex - before);
27
+ const end = Math.min(lastIndex, last + after);
27
28
 
28
29
  return {end, start};
29
30
  }
@@ -33,7 +34,7 @@ function onScroll(this: RenderManager): void {
33
34
 
34
35
  if (!state.active) {
35
36
  requestAnimationFrame(() => {
36
- const top = state.components.body.elements.group.scrollTop;
37
+ const top = state.element.scrollTop;
37
38
 
38
39
  this.update(top > state.top);
39
40
 
@@ -57,10 +58,10 @@ export class RenderManager {
57
58
 
58
59
  state: RenderState;
59
60
 
60
- visible = new Map<number, GroupComponent | Key>();
61
+ visible = new Map<number, Key>();
61
62
 
62
63
  constructor(state: State) {
63
- this.listener = on(state.components.body.elements.group, 'scroll', onScroll.bind(this));
64
+ this.listener = on(state.element, 'scroll', onScroll.bind(this));
64
65
 
65
66
  this.state = {
66
67
  ...state,
@@ -75,6 +76,20 @@ export class RenderManager {
75
76
  listener();
76
77
  visible.clear();
77
78
 
79
+ const cells = Object.values(pool.cells).flat();
80
+
81
+ let {length} = cells;
82
+
83
+ for (let index = 0; index < length; index += 1) {
84
+ cells[index].remove();
85
+ }
86
+
87
+ length = pool.rows.length;
88
+
89
+ for (let index = 0; index < length; index += 1) {
90
+ pool.rows[index].remove();
91
+ }
92
+
78
93
  pool.cells = {};
79
94
  pool.rows = [];
80
95
 
@@ -94,11 +109,11 @@ export class RenderManager {
94
109
  }
95
110
 
96
111
  for (const [, key] of visible) {
97
- if (key instanceof GroupComponent) {
112
+ if (isGroupKey(key)) {
98
113
  continue;
99
114
  }
100
115
 
101
- const row = state.managers.row.get(key);
116
+ const row = state.managers.row.get(key, false);
102
117
 
103
118
  if (row == null || row.element == null) {
104
119
  continue;
@@ -139,17 +154,17 @@ export class RenderManager {
139
154
  let remove = rerender ?? false;
140
155
 
141
156
  for (const [index, key] of visible) {
142
- if (key instanceof GroupComponent) {
157
+ if (isGroupKey(key)) {
143
158
  if (remove || !indices.has(index)) {
144
159
  visible.delete(index);
145
160
 
146
- key.element?.remove();
161
+ state.managers.group.getForKey(key as string)?.element?.remove();
147
162
  }
148
163
 
149
164
  continue;
150
165
  }
151
166
 
152
- const row = managers.row.get(key);
167
+ const row = managers.row.get(key, false);
153
168
 
154
169
  if (remove || row == null || !indices.has(index) || managers.group.collapsed.has(key)) {
155
170
  visible.delete(index);
@@ -174,23 +189,29 @@ export class RenderManager {
174
189
 
175
190
  const key = keys[index];
176
191
 
177
- if (key instanceof GroupComponent) {
192
+ if (isGroupKey(key)) {
193
+ const group = managers.group.getForKey(key as string);
194
+
195
+ if (group == null) {
196
+ continue;
197
+ }
198
+
178
199
  count += 1;
179
200
 
180
- renderGroup(state, key);
201
+ renderGroup(state, group);
181
202
 
182
- visible.set(index, key);
203
+ visible.set(index, group.key);
183
204
 
184
- if (key.element != null) {
185
- key.element.style.transform = `translateY(${(index - offset) * options.rowHeight}px)`;
205
+ if (group.element != null) {
206
+ group.element.style.transform = `translateY(${(index - offset) * options.rowHeight}px)`;
186
207
 
187
- fragment.append(key.element);
208
+ fragment.append(group.element);
188
209
  }
189
210
 
190
211
  continue;
191
212
  }
192
213
 
193
- const row = managers.row.get(key);
214
+ const row = managers.row.get(key, true);
194
215
 
195
216
  if (row == null) {
196
217
  continue;
@@ -215,8 +236,14 @@ export class RenderManager {
215
236
  }
216
237
  }
217
238
 
218
- if (count > 0) {
219
- components.body.elements.group[down ? 'append' : 'prepend'](fragment);
239
+ if (count === 0) {
240
+ return;
241
+ }
242
+
243
+ if (down) {
244
+ components.body.elements.group.append(fragment);
245
+ } else {
246
+ components.body.elements.group.prepend(fragment);
220
247
  }
221
248
  }
222
249
  }
@@ -7,24 +7,30 @@ export class RowManager {
7
7
 
8
8
  constructor(public state: State) {}
9
9
 
10
- destroy(): void {
11
- const components = [...this.components.values()];
12
- const {length} = components;
10
+ clear(): void {
11
+ const {components} = this;
12
+
13
+ const rows = [...components.values()];
14
+ const {length} = rows;
13
15
 
14
16
  for (let index = 0; index < length; index += 1) {
15
- removeRow(this.state.managers.render.pool, components[index]);
17
+ this.removeRow(rows[index]);
16
18
  }
17
19
 
18
- this.components.clear();
20
+ components.clear();
21
+ }
22
+
23
+ destroy(): void {
24
+ this.clear();
19
25
 
20
26
  this.components = undefined as never;
21
27
  this.state = undefined as never;
22
28
  }
23
29
 
24
- get(key: Key): RowComponent | undefined {
30
+ get(key: Key, create: boolean): RowComponent | undefined {
25
31
  let row = this.components.get(key);
26
32
 
27
- if (row == null) {
33
+ if (row == null && create) {
28
34
  row = new RowComponent(key);
29
35
 
30
36
  this.components.set(key, row);
@@ -41,16 +47,22 @@ export class RowManager {
41
47
  const row = this.components.get(key);
42
48
 
43
49
  if (row != null) {
44
- removeRow(this.state.managers.render.pool, row);
50
+ this.removeRow(row);
51
+ }
52
+ }
45
53
 
46
- this.components.delete(key);
54
+ removeRow(row: RowComponent): void {
55
+ if (row.element != null) {
56
+ removeRow(this.state.managers.render.pool, row);
47
57
  }
58
+
59
+ this.components.delete(row.key);
48
60
  }
49
61
 
50
62
  update(key: Key): void {
51
63
  const row = this.components.get(key);
52
64
 
53
- if (row != null) {
65
+ if (row?.element != null) {
54
66
  renderRow(this.state, row);
55
67
  }
56
68
  }