@vaadin/component-base 24.2.0-alpha5 → 24.2.0-dev.f254716fe

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/component-base",
3
- "version": "24.2.0-alpha5",
3
+ "version": "24.2.0-dev.f254716fe",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,5 +42,5 @@
42
42
  "@vaadin/testing-helpers": "^0.4.3",
43
43
  "sinon": "^13.0.2"
44
44
  },
45
- "gitHead": "73db22a5e8993e3ce48d1e6540d30eff9cb5c257"
45
+ "gitHead": "da54950b9f8c14c6451ede0d426e16a489c7fb9b"
46
46
  }
@@ -0,0 +1,226 @@
1
+ export class Cache {
2
+ /**
3
+ * @type {import('../data-provider-controller.js').DataProviderController}
4
+ */
5
+ controller;
6
+
7
+ /**
8
+ * The number of items.
9
+ *
10
+ * @type {number}
11
+ */
12
+ size = 0;
13
+
14
+ /**
15
+ * The number of items to display per page.
16
+ *
17
+ * @type {number}
18
+ */
19
+ pageSize;
20
+
21
+ /**
22
+ * The total number of items, including items from expanded sub-caches.
23
+ *
24
+ * @type {number}
25
+ */
26
+ effectiveSize = 0;
27
+
28
+ /**
29
+ * An array of cached items.
30
+ *
31
+ * @type {object[]}
32
+ */
33
+ items = [];
34
+
35
+ /**
36
+ * A map where the key is a requested page and the value is a callback
37
+ * that will be called with data once the request is complete.
38
+ *
39
+ * @type {Map<number, Function>}
40
+ */
41
+ pendingRequests = new Map();
42
+
43
+ /**
44
+ * A map where the key is the index of an item in the `items` array
45
+ * and the value is a sub-cache associated with that item.
46
+ *
47
+ * Note, it's intentionally defined as an object instead of a Map
48
+ * to ensure that Object.entries() returns an array with keys sorted
49
+ * in alphabetical order, rather than the order they were added.
50
+ *
51
+ * @type {Record<number, Cache>}
52
+ */
53
+ #subCacheByIndex = {};
54
+
55
+ /**
56
+ * @param {Cache['controller']} controller
57
+ * @param {number} pageSize
58
+ * @param {number | undefined} size
59
+ * @param {Cache | undefined} parentCache
60
+ * @param {number | undefined} parentCacheIndex
61
+ */
62
+ constructor(controller, pageSize, size, parentCache, parentCacheIndex) {
63
+ this.controller = controller;
64
+ this.pageSize = pageSize;
65
+ this.size = size || 0;
66
+ this.effectiveSize = size || 0;
67
+ this.parentCache = parentCache;
68
+ this.parentCacheIndex = parentCacheIndex;
69
+ }
70
+
71
+ /**
72
+ * An item in the parent cache that the current cache is associated with.
73
+ *
74
+ * @return {object | undefined}
75
+ */
76
+ get parentItem() {
77
+ return this.parentCache && this.parentCache.items[this.parentCacheIndex];
78
+ }
79
+
80
+ /**
81
+ * Whether the cache or any of its descendant caches have pending requests.
82
+ *
83
+ * @return {boolean}
84
+ */
85
+ get isLoading() {
86
+ if (this.pendingRequests.size > 0) {
87
+ return true;
88
+ }
89
+
90
+ return Object.values(this.#subCacheByIndex).some((subCache) => subCache.isLoading);
91
+ }
92
+
93
+ /**
94
+ * An array of sub-caches sorted in the same order as their associated items
95
+ * appear in the `items` array.
96
+ *
97
+ * @return {Array<[number, Cache]>}
98
+ */
99
+ get subCaches() {
100
+ return Object.entries(this.#subCacheByIndex).map(([index, subCache]) => {
101
+ return [parseInt(index), subCache];
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Recalculates the effective size for the cache and its descendant caches recursively.
107
+ */
108
+ recalculateEffectiveSize() {
109
+ this.effectiveSize =
110
+ !this.parentItem || this.controller.isExpanded(this.parentItem)
111
+ ? this.size +
112
+ Object.values(this.#subCacheByIndex).reduce((total, subCache) => {
113
+ subCache.recalculateEffectiveSize();
114
+ return total + subCache.effectiveSize;
115
+ }, 0)
116
+ : 0;
117
+ }
118
+
119
+ /**
120
+ * Adds an array of items corresponding to the given page
121
+ * to the `items` array.
122
+ *
123
+ * @param {number} page
124
+ * @param {object[]} items
125
+ */
126
+ setPage(page, items) {
127
+ const startIndex = page * this.pageSize;
128
+ items.forEach((item, i) => {
129
+ this.items[startIndex + i] = item;
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Returns whether the given page has been loaded in the cache.
135
+ *
136
+ * @param {number} page
137
+ * @return {boolean}
138
+ */
139
+ isPageLoaded(page) {
140
+ return this.items[page * this.pageSize] !== undefined;
141
+ }
142
+
143
+ /**
144
+ * Retrieves the sub-cache associated with the item at the given index
145
+ * in the `items` array.
146
+ *
147
+ * @param {number} index
148
+ * @return {Cache | undefined}
149
+ */
150
+ getSubCache(index) {
151
+ return this.#subCacheByIndex[index];
152
+ }
153
+
154
+ /**
155
+ * Removes the sub-cache associated with the item at the given index
156
+ * in the `items` array.
157
+ *
158
+ * @param {number} index
159
+ */
160
+ removeSubCache(index) {
161
+ const subCache = this.getSubCache(index);
162
+ delete this.#subCacheByIndex[index];
163
+
164
+ this.controller.dispatchEvent(
165
+ new CustomEvent('sub-cache-removed', {
166
+ detail: {
167
+ cache: this,
168
+ subCache,
169
+ },
170
+ }),
171
+ );
172
+ }
173
+
174
+ /**
175
+ * Removes all sub-caches.
176
+ */
177
+ removeSubCaches() {
178
+ this.#subCacheByIndex = {};
179
+
180
+ this.controller.dispatchEvent(
181
+ new CustomEvent('sub-caches-removed', {
182
+ detail: {
183
+ cache: this,
184
+ },
185
+ }),
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Creates and associates a sub-cache for the item at the given index
191
+ * in the `items` array.
192
+ *
193
+ * @param {number} index
194
+ * @return {Cache}
195
+ */
196
+ createSubCache(index) {
197
+ const subCache = new Cache(this.controller, this.pageSize, 0, this, index);
198
+ this.#subCacheByIndex[index] = subCache;
199
+
200
+ this.controller.dispatchEvent(
201
+ new CustomEvent('sub-cache-created', {
202
+ detail: {
203
+ cache: this,
204
+ subCache,
205
+ },
206
+ }),
207
+ );
208
+
209
+ return subCache;
210
+ }
211
+
212
+ /**
213
+ * Retrieves the flattened index corresponding to the given index
214
+ * of an item in the `items` array.
215
+ *
216
+ * @param {number} index
217
+ * @return {number}
218
+ */
219
+ getFlatIndex(index) {
220
+ const clampedIndex = Math.max(0, Math.min(this.size - 1, index));
221
+
222
+ return this.subCaches.reduce((prev, [index, subCache]) => {
223
+ return clampedIndex > index ? prev + subCache.effectiveSize : prev;
224
+ }, clampedIndex);
225
+ }
226
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Retreives information for the given flattened index, including:
3
+ * - the corresponding cache
4
+ * - the associated item (if loaded)
5
+ * - the corresponding index in the cache's items array.
6
+ * - the page containing the index.
7
+ * - the cache level
8
+ *
9
+ * @param {import('./cache.js').Cache} cache
10
+ * @param {number} flatIndex
11
+ * @return {{ cache: Cache, item: object | undefined, index: number, page: number, level: number }}
12
+ */
13
+ export function getFlatIndexInfo(cache, flatIndex, level = 0) {
14
+ let levelIndex = flatIndex;
15
+
16
+ for (const [index, subCache] of cache.subCaches) {
17
+ if (levelIndex <= index) {
18
+ break;
19
+ } else if (levelIndex <= index + subCache.effectiveSize) {
20
+ return getFlatIndexInfo(subCache, levelIndex - index - 1, level + 1);
21
+ }
22
+ levelIndex -= subCache.effectiveSize;
23
+ }
24
+
25
+ return {
26
+ cache,
27
+ item: cache.items[levelIndex],
28
+ index: levelIndex,
29
+ page: Math.floor(levelIndex / cache.pageSize),
30
+ level,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Recursively returns the globally flat index of the item the given indexes point to.
36
+ * Each index in the array points to a sub-item of the previous index.
37
+ * Using `Infinity` as an index will point to the last item on the level.
38
+ *
39
+ * @param {!ItemCache} cache
40
+ * @param {!Array<number>} indexes
41
+ * @param {number} flatIndex
42
+ * @return {number}
43
+ */
44
+ export function getFlatIndexByPath(cache, [levelIndex, ...subIndexes], flatIndex = 0) {
45
+ if (levelIndex === Infinity) {
46
+ // Treat Infinity as the last index on the level
47
+ levelIndex = cache.size - 1;
48
+ }
49
+
50
+ const flatIndexOnLevel = cache.getFlatIndex(levelIndex);
51
+ const subCache = cache.getSubCache(levelIndex);
52
+ if (subCache && subCache.effectiveSize > 0 && subIndexes.length) {
53
+ return getFlatIndexByPath(subCache, subIndexes, flatIndex + flatIndexOnLevel + 1);
54
+ }
55
+ return flatIndex + flatIndexOnLevel;
56
+ }
@@ -0,0 +1,126 @@
1
+ import { Cache } from './data-provider-controller/cache.js';
2
+ import { getFlatIndexByPath, getFlatIndexInfo } from './data-provider-controller/helpers.js';
3
+
4
+ export class DataProviderController extends EventTarget {
5
+ constructor(host, { size, pageSize, isExpanded, dataProvider, dataProviderParams }) {
6
+ super();
7
+ this.host = host;
8
+ this.size = size;
9
+ this.pageSize = pageSize;
10
+ this.isExpanded = isExpanded;
11
+ this.dataProvider = dataProvider;
12
+ this.dataProviderParams = dataProviderParams;
13
+ this.rootCache = this.#createRootCache();
14
+ }
15
+
16
+ get effectiveSize() {
17
+ return this.rootCache.effectiveSize;
18
+ }
19
+
20
+ isLoading() {
21
+ return this.rootCache.isLoading;
22
+ }
23
+
24
+ setSize(size) {
25
+ const delta = size - this.rootCache.size;
26
+ this.size = size;
27
+ this.rootCache.size += delta;
28
+ this.rootCache.effectiveSize += delta;
29
+ }
30
+
31
+ setPageSize(pageSize) {
32
+ this.pageSize = pageSize;
33
+ this.clearCache();
34
+ }
35
+
36
+ setDataProvider(dataProvider) {
37
+ this.dataProvider = dataProvider;
38
+ this.clearCache();
39
+ }
40
+
41
+ recalculateEffectiveSize() {
42
+ this.rootCache.recalculateEffectiveSize();
43
+ }
44
+
45
+ clearCache() {
46
+ this.rootCache = this.#createRootCache();
47
+ }
48
+
49
+ getFlatIndexInfo(flatIndex) {
50
+ return getFlatIndexInfo(this.rootCache, flatIndex);
51
+ }
52
+
53
+ getFlatIndexByPath(path) {
54
+ return getFlatIndexByPath(this.rootCache, path);
55
+ }
56
+
57
+ ensureFlatIndexLoaded(flatIndex) {
58
+ const { cache, page, item } = this.getFlatIndexInfo(flatIndex);
59
+
60
+ if (!item) {
61
+ this.#loadCachePage(cache, page);
62
+ }
63
+ }
64
+
65
+ ensureFlatIndexChildrenLoaded(flatIndex) {
66
+ const { cache, item, index } = this.getFlatIndexInfo(flatIndex);
67
+
68
+ if (item && this.isExpanded(item)) {
69
+ let subCache = cache.getSubCache(index);
70
+ if (!subCache) {
71
+ subCache = cache.createSubCache(index);
72
+ }
73
+
74
+ if (!subCache.isPageLoaded(0)) {
75
+ this.#loadCachePage(subCache, 0);
76
+ }
77
+ }
78
+ }
79
+
80
+ ensureFirstPageLoaded() {
81
+ if (!this.rootCache.isPageLoaded(0)) {
82
+ this.#loadCachePage(this.rootCache, 0);
83
+ }
84
+ }
85
+
86
+ #createRootCache() {
87
+ return new Cache(this, this.pageSize, this.size);
88
+ }
89
+
90
+ #loadCachePage(cache, page) {
91
+ if (!this.dataProvider || cache.pendingRequests.has(page)) {
92
+ return;
93
+ }
94
+
95
+ const params = {
96
+ page,
97
+ pageSize: this.pageSize,
98
+ parentItem: cache.parentItem,
99
+ ...this.dataProviderParams(),
100
+ };
101
+
102
+ const callback = (items, size) => {
103
+ if (size !== undefined) {
104
+ cache.size = size;
105
+ } else if (params.parentItem) {
106
+ cache.size = items.length;
107
+ }
108
+
109
+ cache.setPage(page, items);
110
+
111
+ this.recalculateEffectiveSize();
112
+
113
+ this.dispatchEvent(new CustomEvent('page-received'));
114
+
115
+ cache.pendingRequests.delete(page);
116
+
117
+ this.dispatchEvent(new CustomEvent('page-loaded'));
118
+ };
119
+
120
+ cache.pendingRequests.set(page, callback);
121
+
122
+ this.dispatchEvent(new CustomEvent('page-requested'));
123
+
124
+ this.dataProvider(params, callback);
125
+ }
126
+ }