@player-ui/player 0.0.1-next.1
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/dist/index.cjs.js +1394 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.esm.js +1326 -0
- package/package.json +26 -0
- package/src/data.ts +247 -0
- package/src/index.ts +18 -0
- package/src/player.ts +497 -0
- package/src/plugins/flow-exp-plugin.ts +65 -0
- package/src/types.ts +114 -0
- package/src/utils/desc.d.ts +2 -0
- package/src/validation/binding-tracker.ts +239 -0
- package/src/validation/controller.ts +661 -0
- package/src/validation/index.ts +2 -0
- package/src/view/asset-transform.ts +147 -0
- package/src/view/controller.ts +148 -0
- package/src/view/index.ts +4 -0
- package/src/view/store.ts +94 -0
- package/src/view/types.ts +31 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { ViewPlugin, Resolver, Node, ViewInstance } from '@player-ui/view';
|
|
2
|
+
import type {
|
|
3
|
+
BindingInstance,
|
|
4
|
+
BindingLike,
|
|
5
|
+
BindingFactory,
|
|
6
|
+
} from '@player-ui/binding';
|
|
7
|
+
import { isBinding } from '@player-ui/binding';
|
|
8
|
+
import type { ValidationResponse } from '@player-ui/validator';
|
|
9
|
+
import type { Validation } from '@player-ui/types';
|
|
10
|
+
|
|
11
|
+
const CONTEXT = 'validation-binding-tracker';
|
|
12
|
+
|
|
13
|
+
export interface BindingTracker {
|
|
14
|
+
/** Get the bindings currently being tracked for validation */
|
|
15
|
+
getBindings(): Set<BindingInstance>;
|
|
16
|
+
}
|
|
17
|
+
interface Options {
|
|
18
|
+
/** Parse a binding from a view */
|
|
19
|
+
parseBinding: BindingFactory;
|
|
20
|
+
|
|
21
|
+
/** Callbacks when events happen */
|
|
22
|
+
callbacks?: {
|
|
23
|
+
/** Called when a binding is encountered for the first time in a view */
|
|
24
|
+
onAdd?: (binding: BindingInstance) => void;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A view plugin that manages bindings tracked across updates */
|
|
29
|
+
export class ValidationBindingTrackerViewPlugin
|
|
30
|
+
implements ViewPlugin, BindingTracker
|
|
31
|
+
{
|
|
32
|
+
private options: Options;
|
|
33
|
+
|
|
34
|
+
private trackedBindings = new Set<BindingInstance>();
|
|
35
|
+
|
|
36
|
+
constructor(options: Options) {
|
|
37
|
+
this.options = options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fetch the tracked bindings in the current view */
|
|
41
|
+
getBindings(): Set<BindingInstance> {
|
|
42
|
+
return this.trackedBindings;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Attach hooks to the given resolver */
|
|
46
|
+
applyResolver(resolver: Resolver) {
|
|
47
|
+
this.trackedBindings.clear();
|
|
48
|
+
|
|
49
|
+
/** Each node maps to a set of bindings that it directly tracks */
|
|
50
|
+
const tracked = new Map<Node.Node, Set<BindingInstance>>();
|
|
51
|
+
|
|
52
|
+
/** Each Node is a registered section or page that maps to a set of nodes in its section */
|
|
53
|
+
const sections = new Map<Node.Node, Set<Node.Node>>();
|
|
54
|
+
|
|
55
|
+
/** Keep track of all seen bindings so we can notify people when we hit one for the first time */
|
|
56
|
+
const seenBindings = new Set<BindingInstance>();
|
|
57
|
+
|
|
58
|
+
let lastViewUpdateChangeSet: Set<BindingInstance> | undefined;
|
|
59
|
+
|
|
60
|
+
const nodeTree = new Map<Node.Node, Set<Node.Node>>();
|
|
61
|
+
|
|
62
|
+
/** Map of node to all bindings in children */
|
|
63
|
+
let lastComputedBindingTree = new Map<Node.Node, Set<BindingInstance>>();
|
|
64
|
+
let currentBindingTree = new Map<Node.Node, Set<BindingInstance>>();
|
|
65
|
+
|
|
66
|
+
/** Map of registered section nodes to bindings */
|
|
67
|
+
const lastSectionBindingTree = new Map<Node.Node, Set<BindingInstance>>();
|
|
68
|
+
|
|
69
|
+
/** Add the given child to the parent's tree. Create the parent entry if none exists */
|
|
70
|
+
function addToTree(child: Node.Node, parent: Node.Node) {
|
|
71
|
+
if (nodeTree.has(parent)) {
|
|
72
|
+
nodeTree.get(parent)?.add(child);
|
|
73
|
+
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
nodeTree.set(parent, new Set([child]));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
resolver.hooks.beforeUpdate.tap(CONTEXT, (changes) => {
|
|
81
|
+
lastViewUpdateChangeSet = changes;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
resolver.hooks.skipResolve.tap(CONTEXT, (shouldSkip, node) => {
|
|
85
|
+
const trackedBindingsForNode = lastComputedBindingTree.get(node);
|
|
86
|
+
|
|
87
|
+
if (!shouldSkip || !lastViewUpdateChangeSet || !trackedBindingsForNode) {
|
|
88
|
+
return shouldSkip;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const intersection = new Set(
|
|
92
|
+
[...lastViewUpdateChangeSet].filter((b) =>
|
|
93
|
+
trackedBindingsForNode.has(b)
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return intersection.size === 0;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
resolver.hooks.resolveOptions.tap(CONTEXT, (options, node) => {
|
|
101
|
+
if (options.validation === undefined) {
|
|
102
|
+
return options;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Clear out any old tracked bindings for this node since we're re-compiling it
|
|
106
|
+
tracked.delete(node);
|
|
107
|
+
|
|
108
|
+
/** Validation callback to track a binding */
|
|
109
|
+
const track = (binding: BindingLike) => {
|
|
110
|
+
const parsed = isBinding(binding)
|
|
111
|
+
? binding
|
|
112
|
+
: this.options.parseBinding(binding);
|
|
113
|
+
|
|
114
|
+
if (tracked.has(node)) {
|
|
115
|
+
tracked.get(node)?.add(parsed);
|
|
116
|
+
} else {
|
|
117
|
+
tracked.set(node, new Set([parsed]));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** find first parent registered as section and add self to its list */
|
|
121
|
+
let { parent } = node;
|
|
122
|
+
|
|
123
|
+
while (parent) {
|
|
124
|
+
if (sections.has(parent)) {
|
|
125
|
+
sections.get(parent)?.add(node);
|
|
126
|
+
break;
|
|
127
|
+
} else {
|
|
128
|
+
parent = parent.parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!seenBindings.has(parsed)) {
|
|
133
|
+
seenBindings.add(parsed);
|
|
134
|
+
this.options.callbacks?.onAdd?.(parsed);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...options,
|
|
140
|
+
validation: {
|
|
141
|
+
...options.validation,
|
|
142
|
+
get: (binding, getOptions) => {
|
|
143
|
+
if (getOptions?.track) {
|
|
144
|
+
track(binding);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const eow = options.validation?._getValidationForBinding(binding);
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
eow?.displayTarget === undefined ||
|
|
151
|
+
eow?.displayTarget === 'field'
|
|
152
|
+
) {
|
|
153
|
+
return eow;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
},
|
|
158
|
+
getChildren: (type: Validation.DisplayTarget) => {
|
|
159
|
+
const validations = new Array<ValidationResponse>();
|
|
160
|
+
lastComputedBindingTree.get(node)?.forEach((binding) => {
|
|
161
|
+
const eow = options.validation?._getValidationForBinding(binding);
|
|
162
|
+
|
|
163
|
+
if (eow && type === eow.displayTarget) {
|
|
164
|
+
validations.push(eow);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return validations;
|
|
169
|
+
},
|
|
170
|
+
getValidationsForSection: () => {
|
|
171
|
+
const validations = new Array<ValidationResponse>();
|
|
172
|
+
lastSectionBindingTree.get(node)?.forEach((binding) => {
|
|
173
|
+
const eow = options.validation?._getValidationForBinding(binding);
|
|
174
|
+
|
|
175
|
+
if (eow && eow.displayTarget === 'section') {
|
|
176
|
+
validations.push(eow);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return validations;
|
|
181
|
+
},
|
|
182
|
+
register: (registerOptions) => {
|
|
183
|
+
if (registerOptions?.type === 'section') {
|
|
184
|
+
if (!sections.has(node)) {
|
|
185
|
+
sections.set(node, new Set());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
track,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
resolver.hooks.afterNodeUpdate.tap(CONTEXT, (node, parent, update) => {
|
|
195
|
+
if (parent) {
|
|
196
|
+
addToTree(node, parent);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Compute the new tree for this node
|
|
200
|
+
// If it's not-updated, use the last known value
|
|
201
|
+
|
|
202
|
+
if (update.updated) {
|
|
203
|
+
const newlyComputed = new Set(tracked.get(node));
|
|
204
|
+
nodeTree.get(node)?.forEach((child) => {
|
|
205
|
+
currentBindingTree.get(child)?.forEach((b) => newlyComputed.add(b));
|
|
206
|
+
});
|
|
207
|
+
currentBindingTree.set(node, newlyComputed);
|
|
208
|
+
} else {
|
|
209
|
+
currentBindingTree.set(
|
|
210
|
+
node,
|
|
211
|
+
lastComputedBindingTree.get(node) ?? new Set()
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (node === resolver.root) {
|
|
216
|
+
this.trackedBindings = currentBindingTree.get(node) ?? new Set();
|
|
217
|
+
lastComputedBindingTree = currentBindingTree;
|
|
218
|
+
|
|
219
|
+
lastSectionBindingTree.clear();
|
|
220
|
+
sections.forEach((nodeSet, sectionNode) => {
|
|
221
|
+
const temp = new Set<BindingInstance>();
|
|
222
|
+
nodeSet.forEach((n) => {
|
|
223
|
+
tracked.get(n)?.forEach(temp.add, temp);
|
|
224
|
+
});
|
|
225
|
+
lastSectionBindingTree.set(sectionNode, temp);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
nodeTree.clear();
|
|
229
|
+
tracked.clear();
|
|
230
|
+
sections.clear();
|
|
231
|
+
currentBindingTree = new Map();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
apply(view: ViewInstance) {
|
|
237
|
+
view.hooks.resolver.tap(CONTEXT, this.applyResolver.bind(this));
|
|
238
|
+
}
|
|
239
|
+
}
|