@microsoft/fast-html 1.0.0-alpha.17 → 1.0.0-alpha.19

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.
@@ -0,0 +1,551 @@
1
+ import { Observable } from "@microsoft/fast-element/observable.js";
2
+ import { assignObservables, getNextProperty } from "./utilities.js";
3
+ const reservedIndexPlaceholder = "__index__";
4
+ /**
5
+ * ObserverMap provides functionality for caching binding paths, extracting root properties,
6
+ * and defining observable properties on class prototypes
7
+ */
8
+ export class ObserverMap {
9
+ constructor(classPrototype) {
10
+ this.cachePaths = {};
11
+ this.contextCache = [];
12
+ /**
13
+ * Creates a property change handler function for observable properties
14
+ * This handler is called when an observable property transitions from undefined to a defined value
15
+ * @param propertyName - The name of the property for which to create the change handler
16
+ * @returns A function that handles property changes and sets up proxies for object values
17
+ */
18
+ this.defineChanged = (propertyName) => {
19
+ const getAndAssignObservablesAlias = this.getAndAssignObservables;
20
+ const cachePaths = this.cachePaths;
21
+ function instanceResolverChanged(prev, next) {
22
+ if (prev === undefined && next !== undefined) {
23
+ const proxy = getAndAssignObservablesAlias(this, propertyName, next, cachePaths);
24
+ this[propertyName] = proxy;
25
+ }
26
+ }
27
+ return instanceResolverChanged;
28
+ };
29
+ this.classPrototype = classPrototype;
30
+ }
31
+ getRootProperty(config) {
32
+ if (config.self) {
33
+ const contextCacheItem = this.contextCache.find(contextCacheItem => {
34
+ return contextCacheItem.context === config.parentContext;
35
+ });
36
+ if (contextCacheItem) {
37
+ if (contextCacheItem.parent) {
38
+ return this.getRootProperty(Object.assign(Object.assign({}, config), { self: true, parentContext: contextCacheItem.parent }));
39
+ }
40
+ return getNextProperty(contextCacheItem.absolutePath);
41
+ }
42
+ }
43
+ return config.self
44
+ ? config.parentContext
45
+ : getNextProperty(config.path);
46
+ }
47
+ /**
48
+ * Caches a binding path with context information for later use in generating observable properties
49
+ * @param context - The path context information containing path, self, parentContext, contextPath, type, and level
50
+ */
51
+ cachePathWithContext(config) {
52
+ // Handle relative path navigation with "../"
53
+ if (config.path.includes("../")) {
54
+ this.handleRelativePathCaching(config);
55
+ return;
56
+ }
57
+ const rootPath = this.getRootProperty(config);
58
+ this.resolveRootAndContextPath(config.type, config.path, rootPath, config.contextPath);
59
+ switch (config.type) {
60
+ case "access":
61
+ this.handleAccessPath(Object.assign(Object.assign({}, config), { rootPath }));
62
+ break;
63
+ case "event":
64
+ // Event handling not implemented yet
65
+ break;
66
+ case "repeat":
67
+ this.handleRepeatPath(Object.assign(Object.assign({}, config), { rootPath }));
68
+ break;
69
+ }
70
+ }
71
+ /**
72
+ * Handles caching of paths with relative navigation ("../")
73
+ * Determines the correct context level and caches the path accordingly
74
+ */
75
+ handleRelativePathCaching(config) {
76
+ // Count the number of "../" to determine how many levels to go up
77
+ const upLevels = this.countRelativeNavigationLevels(config.path);
78
+ // Extract the property name after all the "../" sequences
79
+ const propertyName = this.extractPropertyNameFromRelativePath(config.path);
80
+ // Determine the target context based on navigation level
81
+ const targetContext = this.getTargetContextForRelativePath(config.parentContext, upLevels);
82
+ // Create the absolute path based on where we end up
83
+ const absolutePath = targetContext === null ? propertyName : `${targetContext}.${propertyName}`;
84
+ // Cache the path at the determined context level
85
+ if (targetContext === null) {
86
+ // Cache at root level
87
+ this.cacheAtRootLevel({
88
+ propertyName,
89
+ absolutePath: propertyName,
90
+ type: config.type,
91
+ });
92
+ }
93
+ else {
94
+ // Cache at the specified context level
95
+ this.cacheAtContextLevel({
96
+ propertyName,
97
+ absolutePath,
98
+ targetContext,
99
+ type: config.type,
100
+ });
101
+ }
102
+ }
103
+ /**
104
+ * Counts the number of "../" sequences in a path without using regex
105
+ */
106
+ countRelativeNavigationLevels(path) {
107
+ if (!path || !path.includes("../")) {
108
+ return 0;
109
+ }
110
+ return path.split("../").length - 1;
111
+ }
112
+ /**
113
+ * Extracts the property name from a relative path, removing all "../" sequences
114
+ */
115
+ extractPropertyNameFromRelativePath(path) {
116
+ // Remove all "../" sequences and get the remaining path
117
+ const cleaned = path.replace(/\.\.\//g, "");
118
+ return cleaned;
119
+ }
120
+ /**
121
+ * Determines the target context after navigating up the specified number of levels
122
+ */
123
+ getTargetContextForRelativePath(currentContext, upLevels) {
124
+ var _a;
125
+ if (currentContext === null || upLevels === 0) {
126
+ return null;
127
+ }
128
+ let targetContext = currentContext;
129
+ // Navigate up the context hierarchy
130
+ for (let i = 0; i < upLevels; i++) {
131
+ const contextItem = this.contextCache.find(item => item.context === targetContext);
132
+ targetContext = (_a = contextItem === null || contextItem === void 0 ? void 0 : contextItem.parent) !== null && _a !== void 0 ? _a : null;
133
+ }
134
+ return targetContext;
135
+ }
136
+ /**
137
+ * Caches a path at the root level
138
+ */
139
+ cacheAtRootLevel(config) {
140
+ // For access type, cache the property directly as an access path
141
+ if (config.type === "access") {
142
+ this.cachePaths[config.propertyName] = {
143
+ type: config.type,
144
+ relativePath: config.propertyName,
145
+ absolutePath: config.absolutePath,
146
+ };
147
+ }
148
+ }
149
+ /**
150
+ * Caches a path at a specific context level
151
+ */
152
+ cacheAtContextLevel(config) {
153
+ // Find the context in cache to determine where to place this
154
+ const contextItem = this.contextCache.find(item => item.context === config.targetContext);
155
+ if (!contextItem) {
156
+ // Fallback to root level if context not found
157
+ this.cacheAtRootLevel(config);
158
+ return;
159
+ }
160
+ // For now, cache at root level since the context structure is complex
161
+ // This could be enhanced to place at the exact context level if needed
162
+ this.cacheAtRootLevel(config);
163
+ }
164
+ /**
165
+ * Handles caching of access-type paths (property bindings)
166
+ */
167
+ handleAccessPath(config) {
168
+ if (config.contextPath === null && config.parentContext === null) {
169
+ this.cacheSimpleAccessPath(config);
170
+ return;
171
+ }
172
+ const context = this.findContextInCache(config.parentContext);
173
+ if (!context) {
174
+ this.cacheSimpleAccessPath(config);
175
+ return;
176
+ }
177
+ // Try to place this access path under an existing repeat structure
178
+ if (!this.tryPlaceInExistingRepeat(config.path, context)) {
179
+ this.cacheAccessPathWithContext(Object.assign(Object.assign({}, config), { context }));
180
+ }
181
+ }
182
+ /**
183
+ * Handles caching of repeat-type paths (array/loop contexts)
184
+ */
185
+ handleRepeatPath(config) {
186
+ // Add to context cache first
187
+ this.contextCache.push({
188
+ absolutePath: this.getAbsolutePath(Object.assign(Object.assign({}, config), { type: "repeat" })),
189
+ context: config.contextPath,
190
+ parent: config.parentContext,
191
+ });
192
+ // Create path structure if this is a nested repeat
193
+ if (config.self && config.parentContext) {
194
+ this.createNestedRepeatStructure(config.path, config.parentContext, config.contextPath, config.rootPath);
195
+ }
196
+ }
197
+ /**
198
+ * Caches a simple access path without context complexity
199
+ */
200
+ cacheSimpleAccessPath(config) {
201
+ const cachePaths = [config.rootPath, config.path];
202
+ this.resolveContextPath(cachePaths, {
203
+ type: "access",
204
+ relativePath: config.path,
205
+ absolutePath: this.getAbsolutePath(Object.assign(Object.assign({}, config), { contextPath: null, type: "access" })),
206
+ });
207
+ }
208
+ /**
209
+ * Finds a context item in the temporary context cache
210
+ */
211
+ findContextInCache(parentContext) {
212
+ return this.contextCache.find(item => item.context === parentContext);
213
+ }
214
+ /**
215
+ * Attempts to place an access path under an existing repeat structure
216
+ * @returns true if successfully placed, false otherwise
217
+ */
218
+ tryPlaceInExistingRepeat(path, context) {
219
+ const contextName = context.context;
220
+ for (const [rootKey, rootValue] of Object.entries(this.cachePaths)) {
221
+ if (this.searchAndPlaceInRepeat(rootValue, [rootKey], path, contextName)) {
222
+ return true;
223
+ }
224
+ }
225
+ return false;
226
+ }
227
+ /**
228
+ * Recursively searches for and places access paths in matching repeat structures
229
+ */
230
+ searchAndPlaceInRepeat(pathObj, currentPath, path, contextName) {
231
+ for (const [pathKey, pathValue] of Object.entries(pathObj.paths || {})) {
232
+ const typedPathValue = pathValue;
233
+ if (this.isMatchingRepeatStructure(typedPathValue, contextName)) {
234
+ this.placeAccessInRepeat(currentPath, pathKey, path, contextName);
235
+ return true;
236
+ }
237
+ // Recursively search nested paths
238
+ if (this.canSearchNested(typedPathValue)) {
239
+ if (this.searchAndPlaceInRepeat(typedPathValue, [...currentPath, pathKey], path, contextName)) {
240
+ return true;
241
+ }
242
+ }
243
+ }
244
+ return false;
245
+ }
246
+ /**
247
+ * Checks if a cached path is a repeat structure with matching context
248
+ */
249
+ isMatchingRepeatStructure(pathValue, contextName) {
250
+ return (pathValue.type === "repeat" &&
251
+ pathValue.context === contextName);
252
+ }
253
+ /**
254
+ * Determines if a cached path can be searched for nested structures
255
+ */
256
+ canSearchNested(pathValue) {
257
+ return pathValue.type === "repeat" || pathValue.type === "default";
258
+ }
259
+ /**
260
+ * Places an access path within an existing repeat structure
261
+ */
262
+ placeAccessInRepeat(currentPath, pathKey, path, contextName) {
263
+ const cachePaths = [...currentPath, pathKey, path];
264
+ const absolutePath = this.buildNestedRepeatAbsolutePath({
265
+ currentPath,
266
+ pathKey,
267
+ path,
268
+ contextName,
269
+ });
270
+ this.resolveContextPath(cachePaths, {
271
+ type: "access",
272
+ relativePath: path,
273
+ absolutePath: absolutePath,
274
+ });
275
+ }
276
+ /**
277
+ * Builds absolute paths for nested repeat access patterns
278
+ * @example "root.items.__index__.subitems.__index__.title"
279
+ */
280
+ buildNestedRepeatAbsolutePath(params) {
281
+ let absolutePath = "root";
282
+ // Build path through the hierarchy
283
+ for (let i = 1; i < params.currentPath.length; i++) {
284
+ const segment = params.currentPath[i];
285
+ absolutePath +=
286
+ i === 1 ? `.${segment}` : `.${reservedIndexPlaceholder}.${segment}`;
287
+ }
288
+ // Add the final repeat and property
289
+ const contextPrefix = `${params.contextName}.`;
290
+ const pathWithoutContext = params.path.startsWith(contextPrefix)
291
+ ? params.path.substring(contextPrefix.length)
292
+ : params.path;
293
+ // If the path is just the context name itself, don't append anything after __index__
294
+ absolutePath +=
295
+ params.path === params.contextName
296
+ ? `.${reservedIndexPlaceholder}.${params.pathKey}.${reservedIndexPlaceholder}`
297
+ : `.${reservedIndexPlaceholder}.${params.pathKey}.${reservedIndexPlaceholder}.${pathWithoutContext}`;
298
+ return absolutePath;
299
+ }
300
+ /**
301
+ * Caches access paths that have context information
302
+ */
303
+ cacheAccessPathWithContext(config) {
304
+ const contextPathRelative = this.getRelativeContextPath(config.context);
305
+ // Create repeat structure if needed
306
+ this.ensureRepeatStructureExists(config.rootPath, contextPathRelative, config.context);
307
+ if (config.self && contextPathRelative !== "") {
308
+ const cachePaths = [config.rootPath, contextPathRelative, config.path];
309
+ this.resolveContextPath(cachePaths, {
310
+ type: "access",
311
+ relativePath: config.path,
312
+ absolutePath: this.getAbsolutePath(Object.assign(Object.assign({}, config), { contextPath: null, type: "access" })),
313
+ });
314
+ }
315
+ else {
316
+ this.cacheSimpleAccessPath(config);
317
+ }
318
+ }
319
+ /**
320
+ * Extracts the relative path from a context's absolute path
321
+ * For nested contexts, this should match the cache structure path
322
+ */
323
+ getRelativeContextPath(context) {
324
+ // For nested repeats, we need to find the path in the cache structure
325
+ // The cache is organized as: root.items.users.badges
326
+ // But the absolute path might be: root.items.__index__.users.__index__.badges.__index__
327
+ const absolutePathSplit = context.absolutePath.split(".");
328
+ absolutePathSplit.shift(); // Remove root
329
+ // Remove __index__ placeholders and the final __index__ if present
330
+ const cleanedPath = absolutePathSplit.filter(segment => segment !== reservedIndexPlaceholder);
331
+ // Remove the last segment as it represents the current context position, not the parent path
332
+ if (cleanedPath.length > 1) {
333
+ cleanedPath.pop();
334
+ }
335
+ return cleanedPath.join(".");
336
+ }
337
+ /**
338
+ * Ensures a repeat structure exists in the cache
339
+ */
340
+ ensureRepeatStructureExists(rootPath, contextPathRelative, context) {
341
+ if (contextPathRelative !== "") {
342
+ const rootCachedPath = this.cachePaths[rootPath];
343
+ if (!rootCachedPath.paths[contextPathRelative]) {
344
+ rootCachedPath.paths[contextPathRelative] = {
345
+ type: "repeat",
346
+ context: context.context,
347
+ paths: {},
348
+ };
349
+ }
350
+ }
351
+ }
352
+ /**
353
+ * Creates the cache structure for nested repeat patterns
354
+ */
355
+ createNestedRepeatStructure(path, parentContext, contextPath, rootPath) {
356
+ const context = this.findContextInCache(parentContext);
357
+ if (!context)
358
+ return;
359
+ // For nested repeats, we need to find where the parent context was placed
360
+ // in the cache structure, not use the absolute path calculation
361
+ const parentRepeatPath = this.findParentRepeatPath(parentContext, rootPath);
362
+ const pathSegment = path.split(".").pop();
363
+ const cachePaths = parentRepeatPath
364
+ ? [...parentRepeatPath, pathSegment]
365
+ : [rootPath, pathSegment];
366
+ this.resolveContextPath(cachePaths, {
367
+ type: "repeat",
368
+ context: contextPath,
369
+ paths: {},
370
+ });
371
+ }
372
+ /**
373
+ * Finds the cache path where a parent context's repeat structure is located
374
+ */
375
+ findParentRepeatPath(parentContext, rootPath) {
376
+ // Search through the cache structure to find where this context is defined
377
+ const searchInStructure = (obj, currentPath) => {
378
+ if (obj.type === "repeat" && obj.context === parentContext) {
379
+ return currentPath;
380
+ }
381
+ if (obj.paths) {
382
+ for (const [key, value] of Object.entries(obj.paths)) {
383
+ const result = searchInStructure(value, [...currentPath, key]);
384
+ if (result)
385
+ return result;
386
+ }
387
+ }
388
+ return null;
389
+ };
390
+ return searchInStructure(this.cachePaths[rootPath], [rootPath]);
391
+ }
392
+ getCachedPathsWithContext() {
393
+ return this.cachePaths;
394
+ }
395
+ resolveContextPath(cachePaths, cachePath) {
396
+ const tempCachePathLastItem = cachePaths.length - 1;
397
+ cachePaths.reduce((previousValue, tempCachePath, index) => {
398
+ var _a;
399
+ if (index === tempCachePathLastItem) {
400
+ // Ensure the previous value has a paths property
401
+ if (!previousValue.paths) {
402
+ previousValue.paths = {};
403
+ }
404
+ previousValue.paths[tempCachePath] = cachePath;
405
+ return previousValue;
406
+ }
407
+ // Navigate to the next level
408
+ const nextValue = index === 0
409
+ ? previousValue[tempCachePath]
410
+ : (_a = previousValue.paths) === null || _a === void 0 ? void 0 : _a[tempCachePath];
411
+ // Ensure the next value exists and has paths property if needed
412
+ if (!nextValue) {
413
+ throw new Error(`Cannot resolve context path: missing intermediate path at '${tempCachePath}'`);
414
+ }
415
+ return nextValue;
416
+ }, this.cachePaths);
417
+ }
418
+ resolveRootAndContextPath(type, path, rootPath, contextPath) {
419
+ switch (type) {
420
+ case "access": {
421
+ const containsContext = this.contextCache.find(contextCacheItem => {
422
+ return contextCacheItem.context === rootPath;
423
+ });
424
+ // add a root path if one has not been assigned
425
+ if (!this.cachePaths[rootPath] && !containsContext) {
426
+ this.cachePaths[rootPath] = {
427
+ type: "default",
428
+ paths: {},
429
+ };
430
+ }
431
+ break;
432
+ }
433
+ case "repeat": {
434
+ // add a context path if one has not been assigned
435
+ // add a root path if one has not been assigned
436
+ if (rootPath === path) {
437
+ this.cachePaths[rootPath] = {
438
+ type: "repeat",
439
+ context: contextPath,
440
+ paths: {},
441
+ };
442
+ }
443
+ break;
444
+ }
445
+ }
446
+ }
447
+ getAbsolutePath(config) {
448
+ const splitPath = [];
449
+ const contextSplitPath = config.contextPath
450
+ ? config.contextPath.split(".")
451
+ : [];
452
+ // Split path by "../" and handle each part
453
+ const pathParts = config.path.split("../");
454
+ pathParts.forEach(pathItem => {
455
+ if (pathItem === "") {
456
+ splitPath.unshift("../");
457
+ }
458
+ else {
459
+ splitPath.push(...pathItem.split("."));
460
+ }
461
+ });
462
+ // Handle level-based path resolution
463
+ for (let i = config.level; i > 0; i--) {
464
+ if (splitPath[0] === "../") {
465
+ splitPath.shift();
466
+ }
467
+ else {
468
+ contextSplitPath.pop();
469
+ }
470
+ }
471
+ const contextPathUpdated = contextSplitPath.join(".");
472
+ if (config.self) {
473
+ // For array items, remove the context prefix and build full path
474
+ splitPath.shift();
475
+ // Build the full path by recursively traversing contextCache
476
+ const fullContextPath = this.getPathFromCachedContext(config.parentContext, config.contextPath);
477
+ const pathSuffix = splitPath.join(".");
478
+ if (fullContextPath) {
479
+ return `${fullContextPath}.${reservedIndexPlaceholder}.${pathSuffix}`;
480
+ }
481
+ else {
482
+ return `${reservedIndexPlaceholder}.${pathSuffix}`;
483
+ }
484
+ }
485
+ if (config.type === "repeat") {
486
+ return splitPath.join(".");
487
+ }
488
+ const pathSuffix = splitPath.join(".");
489
+ if (contextPathUpdated) {
490
+ return `${contextPathUpdated}.${pathSuffix}`;
491
+ }
492
+ else {
493
+ return pathSuffix;
494
+ }
495
+ }
496
+ /**
497
+ * Builds the full context path by looking up parent contexts in contextCache
498
+ * @param parentContext - The immediate parent context to start from
499
+ * @param contextPath - The current context path (like "items")
500
+ * @returns The full absolute path including all parent contexts
501
+ */
502
+ getPathFromCachedContext(parentContext, contextPath) {
503
+ if (!parentContext) {
504
+ return contextPath || "";
505
+ }
506
+ // Find the parent context in contextCache
507
+ const parentContextItem = this.contextCache.find(item => item.context === parentContext);
508
+ if (!parentContextItem) {
509
+ return contextPath || "";
510
+ }
511
+ // The parent's absolutePath is the base path we want to use
512
+ // For array access, we add __index__ between parent and child paths
513
+ const parentAbsolutePath = parentContextItem.absolutePath;
514
+ if (contextPath) {
515
+ // If we have a contextPath, add it to the parent path with __index__
516
+ // Remove trailing dot if present
517
+ const cleanParentPath = parentAbsolutePath.endsWith(".")
518
+ ? parentAbsolutePath.slice(0, -1)
519
+ : parentAbsolutePath;
520
+ return `${cleanParentPath}.${reservedIndexPlaceholder}.${contextPath}`;
521
+ }
522
+ else {
523
+ // If no contextPath, just return the parent's path - this is the base context
524
+ // Remove trailing dot if present
525
+ return parentAbsolutePath.endsWith(".")
526
+ ? parentAbsolutePath.slice(0, -1)
527
+ : parentAbsolutePath;
528
+ }
529
+ }
530
+ defineProperties() {
531
+ Object.keys(this.cachePaths).forEach(propertyName => {
532
+ Observable.defineProperty(this.classPrototype, propertyName);
533
+ this.classPrototype[`${propertyName}Changed`] =
534
+ this.defineChanged(propertyName);
535
+ });
536
+ }
537
+ /**
538
+ * Creates a proxy for an object that intercepts property mutations and triggers Observable notifications
539
+ * @param target - The target instance that owns the root property
540
+ * @param rootProperty - The name of the root property for notification purposes
541
+ * @param object - The object to wrap with a proxy
542
+ * @returns A proxy that triggers notifications on property mutations
543
+ */
544
+ getAndAssignObservables(target, rootProperty, object, cachePaths) {
545
+ let proxiedObject = object;
546
+ if (cachePaths[rootProperty]) {
547
+ proxiedObject = assignObservables(cachePaths[rootProperty], proxiedObject, target, rootProperty);
548
+ }
549
+ return proxiedObject;
550
+ }
551
+ }