-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobserver.js
More file actions
258 lines (213 loc) · 6.49 KB
/
observer.js
File metadata and controls
258 lines (213 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import { ArrayTrap } from './array-trap.js';
import { PropertyTrap } from './property-trap.js';
import { values, entries, privateSymbol } from './utils.js';
const parentSymbol = Symbol('parent');
const nameSymbol = Symbol('name');
const originalSymbol = Symbol('original');
function defineProperty(obj, key, value) {
if (value === undefined) return;
Object.defineProperty(obj, key, {
writable: true,
value,
});
}
function defineParentsAndNames(obj, parent, name) {
let seen = new Set();
function recursively(obj, parent, name) {
if (seen.has(obj)) {
return;
} else {
seen.add(obj);
}
defineProperty(obj, parentSymbol, parent);
defineProperty(obj, nameSymbol, name);
for (let [key, child] of entries(obj)) {
if (child && typeof child == 'object') {
recursively(child, obj, key);
}
}
}
recursively(obj, parent, name);
}
function parent(obj) {
return obj[parentSymbol];
}
function parents(obj, andSelf, until) {
var res = [];
if (obj) {
andSelf && res.push(obj);
while ((obj = obj[parentSymbol])) {
res.unshift(obj);
if (until && until(obj)) break;
}
}
return res;
}
function name(obj) {
return obj[nameSymbol];
}
function path(target, key, targetIsParents) {
return arrayPath.apply(this, arguments).join('/');
/*
let p = (targetIsParents ? target : parents(target, true)).map(name).join('/');
if (key !== undefined) p += '/' + key;
return p.charAt(0) == '/' ? p.substr(1) : p;
*/
}
function arrayPath(target, key, targetIsParents) {
let p = (targetIsParents ? target : parents(target, true)).map(name);
if (key !== undefined) p.push(key);
// root has no name
if (!p[0]) p.shift();
return p;
}
function retrieve(obj, p = '/') {
if (p !== String(p)) return;
if (p == '/') return obj;
if (p.charAt(0) == '/') p = p.substr(1);
let keys = p.split('/');
if (name(obj)) keys.shift();
for (let key of values(keys)) {
obj = obj[key];
}
return obj;
}
function redefineArrayNames(target) {
if (Array.isArray(target)) {
for (let [index, child] of target.entries()) {
if (typeof child == 'object' && child) {
child[nameSymbol] = index;
}
}
}
}
let win = globalThis;
/*
VALIDATE: target, ...args
target is the object changed
args:
on object:
'delete', property, valueRef
'set', property, valueRef, prev
on object property validation valueRef is object with one property called value and can be changed. For example if incorrect type
of value is provided, then inside validation it may be cast to correct type
on array:
name of actual function called, array of actual arguments passed to that array function
on array function call validation the arguments the arguments can be changed
ONCHANGE: target, ...args
target is the object changed
args:
on object property or array direct access:
'delete', property, value, prev
'set', property, value, prev
on array mutation functions or direct length change:
'splice', start, array of deleted elements, array of inserted elements
'sort', array of changes in format [newIndex, prevIndex]
'reverse'
'copyWithin', target, start, array of overwritten elements;
'fill', fillValue, start, array of overwritten elements;
'length', len, prev
array mutation functions 'push', 'pop', 'shift', 'unshift' are reported as calls to 'splice'
*/
let map = new WeakMap();
function createObserver(
source,
{
onchange = () => {},
validate = () => {
return true;
},
equal = (a, b) => {
return a == b;
},
name = undefined,
patchObjects = undefined,
exclude = () => {
return false;
},
} = {}
) {
let arrayTrap = new ArrayTrap();
let propertyTrap = new PropertyTrap();
defineParentsAndNames(source, null, name);
let arrayIsMutating;
function excludeProperty(prop) {
// DOM cannot be proxied (no traps will fire) and if done, the proxy cannot be used f.e appendChild(proxiedNode) will throw
if (typeof HTMLElement == 'function' && prop instanceof HTMLElement) {
return true;
}
return exclude(prop);
}
let handler = {
get(target, key) {
let prop = target[key];
if (Array.isArray(target) && typeof prop == 'function' && arrayTrap.functions.includes(key)) {
return function (...args) {
if (!validate(target, key, args)) return;
arrayIsMutating = true;
let res = arrayTrap[key](target, ...args);
if (arrayTrap.change.length) {
redefineArrayNames(target);
onchange(target, ...arrayTrap.change);
}
arrayIsMutating = false;
return res;
};
}
let typeStr = Object.prototype.toString.call(prop);
if ((typeStr == '[object Object]' || typeStr == '[object Array]') && prop && key != parentSymbol && key != privateSymbol && !Object.getOwnPropertyDescriptor(prop, 'get') && typeof target[nameSymbol] != 'symbol') {
let proxy = map.get(prop);
if (!proxy) {
if (prop[nameSymbol] !== key) {
defineParentsAndNames(prop, target, key);
}
proxy = new Proxy(prop, handler);
map.set(prop, proxy);
}
return proxy;
} else {
return prop;
}
},
set(target, key, value, receiver) {
if (arrayIsMutating) return true;
if ((typeof value == 'function' || typeof target[nameSymbol] == 'symbol' || typeof key == 'symbol') && target[key] != value) {
target[key] = value;
return true;
}
if (!equal(target[key], value)) {
if (Array.isArray(target) && key == 'length') {
let res = arrayTrap[key](target, value);
if (arrayTrap.change.length) {
redefineArrayNames(target);
onchange(target, ...arrayTrap.change);
}
return res;
}
let patched = -1;
if (patchObjects) {
patched = patchObjects(receiver[key], value);
}
if (~patched) {
return true;
} else {
let res = propertyTrap.setProperty(target, key, value, validate);
res && onchange(target, ...propertyTrap.change);
return res;
}
}
return true;
},
deleteProperty(target, key) {
let res = propertyTrap.deleteProperty(target, key, validate);
res && onchange(target, ...propertyTrap.change);
return res;
},
};
let proxy = new win.Proxy(source, handler);
proxy[originalSymbol] = function () {
return source;
};
return proxy;
}
export { originalSymbol, parentSymbol, nameSymbol, parent, path, retrieve, createObserver, arrayPath, defineParentsAndNames };