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

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.e9803eea7",
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": "a065b79b9d5a189e457fab312cc8aff0d7f2f910"
46
46
  }
@@ -0,0 +1,216 @@
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
+ * Retrieves the sub-cache associated with the item at the given index
135
+ * in the `items` array.
136
+ *
137
+ * @param {number} index
138
+ * @return {Cache | undefined}
139
+ */
140
+ getSubCache(index) {
141
+ return this.#subCacheByIndex[index];
142
+ }
143
+
144
+ /**
145
+ * Removes the sub-cache associated with the item at the given index
146
+ * in the `items` array.
147
+ *
148
+ * @param {number} index
149
+ */
150
+ removeSubCache(index) {
151
+ const subCache = this.getSubCache(index);
152
+ delete this.#subCacheByIndex[index];
153
+
154
+ this.controller.dispatchEvent(
155
+ new CustomEvent('sub-cache-removed', {
156
+ detail: {
157
+ cache: this,
158
+ subCache,
159
+ },
160
+ }),
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Removes all sub-caches.
166
+ */
167
+ removeSubCaches() {
168
+ this.#subCacheByIndex = {};
169
+
170
+ this.controller.dispatchEvent(
171
+ new CustomEvent('sub-caches-removed', {
172
+ detail: {
173
+ cache: this,
174
+ },
175
+ }),
176
+ );
177
+ }
178
+
179
+ /**
180
+ * Creates and associates a sub-cache for the item at the given index
181
+ * in the `items` array.
182
+ *
183
+ * @param {number} index
184
+ * @return {Cache}
185
+ */
186
+ createSubCache(index) {
187
+ const subCache = new Cache(this.controller, this.pageSize, 0, this, index);
188
+ this.#subCacheByIndex[index] = subCache;
189
+
190
+ this.controller.dispatchEvent(
191
+ new CustomEvent('sub-cache-created', {
192
+ detail: {
193
+ cache: this,
194
+ subCache,
195
+ },
196
+ }),
197
+ );
198
+
199
+ return subCache;
200
+ }
201
+
202
+ /**
203
+ * Retrieves the flattened index corresponding to the given index
204
+ * of an item in the `items` array.
205
+ *
206
+ * @param {number} index
207
+ * @return {number}
208
+ */
209
+ getFlatIndex(index) {
210
+ const clampedIndex = Math.max(0, Math.min(this.size - 1, index));
211
+
212
+ return this.subCaches.reduce((prev, [index, subCache]) => {
213
+ return clampedIndex > index ? prev + subCache.effectiveSize : prev;
214
+ }, clampedIndex);
215
+ }
216
+ }
@@ -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,116 @@
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
+ }
34
+
35
+ setDataProvider(dataProvider) {
36
+ this.dataProvider = dataProvider;
37
+ }
38
+
39
+ recalculateEffectiveSize() {
40
+ this.rootCache.recalculateEffectiveSize();
41
+ }
42
+
43
+ clearCache() {
44
+ this.rootCache = this.#createRootCache();
45
+ }
46
+
47
+ getFlatIndexInfo(flatIndex) {
48
+ return getFlatIndexInfo(this.rootCache, flatIndex);
49
+ }
50
+
51
+ getFlatIndexByPath(path) {
52
+ return getFlatIndexByPath(this.rootCache, path);
53
+ }
54
+
55
+ ensureFlatIndexLoaded(flatIndex) {
56
+ const { cache, page, item } = this.getFlatIndexInfo(flatIndex);
57
+
58
+ if (!item) {
59
+ this.#loadCachePage(cache, page);
60
+ }
61
+ }
62
+
63
+ ensureFlatIndexHierarchy(flatIndex) {
64
+ const { cache, item, index } = this.getFlatIndexInfo(flatIndex);
65
+
66
+ if (item && this.isExpanded(item) && !cache.getSubCache(index)) {
67
+ const subCache = cache.createSubCache(index);
68
+ this.#loadCachePage(subCache, 0);
69
+ }
70
+ }
71
+
72
+ loadFirstPage() {
73
+ this.#loadCachePage(this.rootCache, 0);
74
+ }
75
+
76
+ #createRootCache() {
77
+ return new Cache(this, this.pageSize, this.size);
78
+ }
79
+
80
+ #loadCachePage(cache, page) {
81
+ if (!this.dataProvider || cache.pendingRequests.has(page)) {
82
+ return;
83
+ }
84
+
85
+ const params = {
86
+ page,
87
+ pageSize: this.pageSize,
88
+ parentItem: cache.parentItem,
89
+ ...this.dataProviderParams(),
90
+ };
91
+
92
+ const callback = (items, size) => {
93
+ if (size !== undefined) {
94
+ cache.size = size;
95
+ } else if (params.parentItem) {
96
+ cache.size = items.length;
97
+ }
98
+
99
+ cache.setPage(page, items);
100
+
101
+ this.recalculateEffectiveSize();
102
+
103
+ this.dispatchEvent(new CustomEvent('page-received'));
104
+
105
+ cache.pendingRequests.delete(page);
106
+
107
+ this.dispatchEvent(new CustomEvent('page-loaded'));
108
+ };
109
+
110
+ cache.pendingRequests.set(page, callback);
111
+
112
+ this.dispatchEvent(new CustomEvent('page-requested'));
113
+
114
+ this.dataProvider(params, callback);
115
+ }
116
+ }