@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.
@@ -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
+ }