resonantjs 1.1.1 → 1.1.3
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/examples/example-basic.html +116 -4
- package/package.json +1 -1
- package/resonant.js +275 -152
- package/resonant.min.js +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
rel="stylesheet"
|
|
7
7
|
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css"
|
|
8
8
|
/>
|
|
9
|
-
<script src="
|
|
9
|
+
<script src="../resonant.js"></script>
|
|
10
10
|
<style>
|
|
11
11
|
section.grid {
|
|
12
12
|
padding: 1rem;
|
|
@@ -40,6 +40,17 @@
|
|
|
40
40
|
<button onclick="counter++">Increment Counter</button>
|
|
41
41
|
</section>
|
|
42
42
|
|
|
43
|
+
<section class="grid">
|
|
44
|
+
<code class="code">
|
|
45
|
+
resonantJs.add("counter", <span res="counter">0</span>);
|
|
46
|
+
</code>
|
|
47
|
+
<code class="code">
|
|
48
|
+
Counter: <span res="counter"></span><br/><br/>
|
|
49
|
+
<button onclick="counter++">Increment Counter</button><br/><br/>
|
|
50
|
+
<div res-display="counter < 10">contents</div><br/>
|
|
51
|
+
</code>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
43
54
|
<!-- Demonstrate object property binding -->
|
|
44
55
|
<section class="grid" res="user">
|
|
45
56
|
<div>
|
|
@@ -67,6 +78,26 @@
|
|
|
67
78
|
</div>
|
|
68
79
|
</section>
|
|
69
80
|
|
|
81
|
+
<section class="grid" res="user">
|
|
82
|
+
<code class="code">
|
|
83
|
+
resonantJs.add({<br/>
|
|
84
|
+
user: {<br/>
|
|
85
|
+
firstname: "<span res-prop="firstname"></span>",<br/>
|
|
86
|
+
lastname: "<span res-prop="lastname"></span>"<br/>
|
|
87
|
+
}<br/>
|
|
88
|
+
});
|
|
89
|
+
</code>
|
|
90
|
+
<code class="code">
|
|
91
|
+
<div res="user"><br/>
|
|
92
|
+
<input type="text" res-prop="firstname" /><br/>
|
|
93
|
+
<input type="text" res-prop="lastname" /><br/>
|
|
94
|
+
<span res-prop="firstname"></span><br/>
|
|
95
|
+
<span res-prop="lastname"></span><br/>
|
|
96
|
+
</div>
|
|
97
|
+
<br/><br/>
|
|
98
|
+
</code>
|
|
99
|
+
</section>
|
|
100
|
+
|
|
70
101
|
<!-- Demonstrate dynamic list rendering -->
|
|
71
102
|
<section class="grid">
|
|
72
103
|
<div>
|
|
@@ -77,8 +108,84 @@
|
|
|
77
108
|
</li>
|
|
78
109
|
</ul>
|
|
79
110
|
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<button onclick="addProjectMember()">Add</button>
|
|
113
|
+
<button onclick="toggleProjectMemberName()">Toggle Alice's Name</button>
|
|
114
|
+
</div>
|
|
80
115
|
|
|
81
|
-
|
|
116
|
+
</section>
|
|
117
|
+
|
|
118
|
+
<section class="grid">
|
|
119
|
+
<code class="code">
|
|
120
|
+
resonantJs.add({<br/>
|
|
121
|
+
projectTeam: [<br/>
|
|
122
|
+
{ name: "Alice", role: "Developer" },<br/>
|
|
123
|
+
{ name: "Bob", role: "Designer" }<br/>
|
|
124
|
+
]<br/>
|
|
125
|
+
});<br/><br/>
|
|
126
|
+
|
|
127
|
+
function addProjectMember() {<br/>
|
|
128
|
+
const newMember = { name: "Charlie", role: "Product Manager" };<br/>
|
|
129
|
+
projectTeam.push(newMember);<br/>
|
|
130
|
+
}
|
|
131
|
+
</code>
|
|
132
|
+
<code class="code">
|
|
133
|
+
<ul res="projectTeam"><br/>
|
|
134
|
+
<li><br/>
|
|
135
|
+
<span res-prop="name"></span> - <span res-prop="role"></span><br/>
|
|
136
|
+
</li><br/>
|
|
137
|
+
</ul><br/><br/>
|
|
138
|
+
<button onclick="addProjectMember()">Add Project Member</button>
|
|
139
|
+
</code>
|
|
140
|
+
</section>
|
|
141
|
+
<section>
|
|
142
|
+
<h1>Summary</h1>
|
|
143
|
+
<p>
|
|
144
|
+
Resonant.js is a simple library that allows you to bind JavaScript variables to HTML elements.
|
|
145
|
+
This allows you to create dynamic web applications without the need for complex frameworks.
|
|
146
|
+
</p>
|
|
147
|
+
<h2>HTML properties</h2>
|
|
148
|
+
<table>
|
|
149
|
+
<tr>
|
|
150
|
+
<td>res</td>
|
|
151
|
+
<td>Bind an object to a section of HTML</td>
|
|
152
|
+
</tr>
|
|
153
|
+
<tr>
|
|
154
|
+
<td>res-prop</td>
|
|
155
|
+
<td>Bind an object property to an HTML element, used when the defined variable is an object array</td>
|
|
156
|
+
</tr>
|
|
157
|
+
<tr>
|
|
158
|
+
<td>res-display</td>
|
|
159
|
+
<td>Conditionally display an element based on a JavaScript expression</td>
|
|
160
|
+
</tr>
|
|
161
|
+
<tr>
|
|
162
|
+
<td>res-style, res-styles</td>
|
|
163
|
+
<td>Bind a style or styles object to an HTML element conditionally</td>
|
|
164
|
+
</tr>
|
|
165
|
+
<tr>
|
|
166
|
+
<td>res-onclick, res-onclick-remove</td>
|
|
167
|
+
<td>Bind an onclick event to an HTML element</td>
|
|
168
|
+
</tr>
|
|
169
|
+
</table>
|
|
170
|
+
<h2>Javascript Functions</h2>
|
|
171
|
+
<table>
|
|
172
|
+
<tr>
|
|
173
|
+
<td>const resonantJs = new Resonant();</td>
|
|
174
|
+
<td>Initialize the Resonant object</td>
|
|
175
|
+
</tr>
|
|
176
|
+
<tr>
|
|
177
|
+
<td>resonantJs.add("variableName", optionalBooleanForPersistence)</td>
|
|
178
|
+
<td>Bind a variable to the Resonant object</td>
|
|
179
|
+
</tr>
|
|
180
|
+
<tr>
|
|
181
|
+
<td>resonantJs.addAll()</td>
|
|
182
|
+
<td>Bind multiple variables to the Resonant object at once</td>
|
|
183
|
+
</tr>
|
|
184
|
+
<tr>
|
|
185
|
+
<td>resonantJs.addCallback("variableName", (objectReturned) => {})</td>
|
|
186
|
+
<td>Bind a callback function to a variable</td>
|
|
187
|
+
</tr>
|
|
188
|
+
</table>
|
|
82
189
|
</section>
|
|
83
190
|
</main>
|
|
84
191
|
|
|
@@ -92,8 +199,7 @@
|
|
|
92
199
|
resonantJs.addAll({
|
|
93
200
|
user: {
|
|
94
201
|
firstname: "John",
|
|
95
|
-
lastname: "Doe"
|
|
96
|
-
email: ""
|
|
202
|
+
lastname: "Doe"
|
|
97
203
|
},
|
|
98
204
|
projectTeam: [
|
|
99
205
|
{ name: "Alice", role: "Developer" },
|
|
@@ -116,6 +222,12 @@
|
|
|
116
222
|
const newMember = { name: "Charlie", role: "Product Manager" };
|
|
117
223
|
projectTeam.push(newMember);
|
|
118
224
|
}
|
|
225
|
+
|
|
226
|
+
//Silly function to demonstrate dynamic list rerendering
|
|
227
|
+
function toggleProjectMemberName() {
|
|
228
|
+
const alice = projectTeam[0];
|
|
229
|
+
alice.name = alice.name === "Alice" ? "Alicia" : "Alice";
|
|
230
|
+
}
|
|
119
231
|
</script>
|
|
120
232
|
</body>
|
|
121
233
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resonantjs",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "A lightweight JavaScript framework that enables reactive data-binding for building dynamic and responsive web applications. It simplifies creating interactive UIs by automatically updating the DOM when your data changes.",
|
|
5
5
|
"main": "resonant.js",
|
|
6
6
|
"repository": {
|
package/resonant.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
class ObservableArray extends Array {
|
|
2
2
|
constructor(variableName, resonantInstance, ...args) {
|
|
3
|
-
if(resonantInstance === undefined) {
|
|
3
|
+
if (resonantInstance === undefined) {
|
|
4
4
|
return super(...args);
|
|
5
5
|
}
|
|
6
6
|
super(...args);
|
|
@@ -13,6 +13,7 @@ class ObservableArray extends Array {
|
|
|
13
13
|
|
|
14
14
|
this.forEach((item, index) => {
|
|
15
15
|
if (typeof item === 'object') {
|
|
16
|
+
this._setKeyForObjectAndChildObjects(item, index);
|
|
16
17
|
this[index] = this._createProxy(item, index);
|
|
17
18
|
}
|
|
18
19
|
});
|
|
@@ -22,12 +23,53 @@ class ObservableArray extends Array {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
_setKeyForObjectAndChildObjects(obj, index) {
|
|
27
|
+
if (typeof obj === 'object') {
|
|
28
|
+
obj.key = index + '-' + this._generateKeyByContentCheckSum(obj);
|
|
29
|
+
Object.keys(obj).forEach(key => {
|
|
30
|
+
this._setKeyForObjectAndChildObjects(obj[key], index);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_generateKeyByContentCheckSum(obj) {
|
|
36
|
+
const removeKeysRecursively = (obj) => {
|
|
37
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(obj)) {
|
|
40
|
+
return obj.map(item => removeKeysRecursively(item));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const newObj = {...obj};
|
|
44
|
+
delete newObj.key;
|
|
45
|
+
|
|
46
|
+
Object.keys(newObj).forEach(key => {
|
|
47
|
+
if (typeof newObj[key] === 'object' && newObj[key] !== null) {
|
|
48
|
+
newObj[key] = removeKeysRecursively(newObj[key]);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return newObj;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const objForHash = removeKeysRecursively(obj);
|
|
56
|
+
const str = JSON.stringify(objForHash);
|
|
57
|
+
let hash = 0;
|
|
58
|
+
for (let i = 0; i < str.length; i++) {
|
|
59
|
+
const char = str.charCodeAt(i);
|
|
60
|
+
hash = ((hash << 5) - hash) + char;
|
|
61
|
+
hash = hash & hash;
|
|
62
|
+
}
|
|
63
|
+
return Math.abs(hash).toString(36);
|
|
64
|
+
}
|
|
65
|
+
|
|
25
66
|
_createProxy(item, index) {
|
|
26
67
|
return new Proxy(item, {
|
|
27
68
|
set: (target, property, value) => {
|
|
28
69
|
if (target[property] !== value) {
|
|
29
70
|
const oldValue = target[property];
|
|
30
71
|
target[property] = value;
|
|
72
|
+
this._setKeyForObjectAndChildObjects(item, index);
|
|
31
73
|
this.resonantInstance._queueUpdate(this.variableName, 'modified', target, property, oldValue, index);
|
|
32
74
|
}
|
|
33
75
|
return true;
|
|
@@ -35,8 +77,11 @@ class ObservableArray extends Array {
|
|
|
35
77
|
});
|
|
36
78
|
}
|
|
37
79
|
|
|
38
|
-
//temp fix for issues
|
|
39
80
|
forceUpdate() {
|
|
81
|
+
this.forEach((item, index) => {
|
|
82
|
+
console.log('forceUpdate', item, index);
|
|
83
|
+
this._setKeyForObjectAndChildObjects(item, index);
|
|
84
|
+
});
|
|
40
85
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
41
86
|
this.resonantInstance._queueUpdate(this.variableName, 'modified', this.slice());
|
|
42
87
|
}
|
|
@@ -49,6 +94,7 @@ class ObservableArray extends Array {
|
|
|
49
94
|
push(...args) {
|
|
50
95
|
args = args.map((item, index) => {
|
|
51
96
|
if (typeof item === 'object') {
|
|
97
|
+
this._setKeyForObjectAndChildObjects(item, this.length + index);
|
|
52
98
|
return this._createProxy(item, this.length + index);
|
|
53
99
|
}
|
|
54
100
|
return item;
|
|
@@ -102,9 +148,9 @@ class ObservableArray extends Array {
|
|
|
102
148
|
}
|
|
103
149
|
|
|
104
150
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
105
|
-
|
|
106
151
|
const oldValue = this[index];
|
|
107
152
|
this[index] = value;
|
|
153
|
+
|
|
108
154
|
this.resonantInstance._queueUpdate(this.variableName, action, this[index], index, oldValue);
|
|
109
155
|
}
|
|
110
156
|
return true;
|
|
@@ -114,9 +160,7 @@ class ObservableArray extends Array {
|
|
|
114
160
|
const oldValue = this[index];
|
|
115
161
|
this.isDeleting = true;
|
|
116
162
|
this.splice(index, 1);
|
|
117
|
-
|
|
118
163
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
119
|
-
|
|
120
164
|
this.resonantInstance._queueUpdate(this.variableName, 'removed', null, index, oldValue);
|
|
121
165
|
this.isDeleting = false;
|
|
122
166
|
return true;
|
|
@@ -126,7 +170,6 @@ class ObservableArray extends Array {
|
|
|
126
170
|
if(this.resonantInstance === undefined || actuallyFilter === false) {
|
|
127
171
|
return super.filter(filter);
|
|
128
172
|
}
|
|
129
|
-
|
|
130
173
|
const result = super.filter(filter);
|
|
131
174
|
this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
|
|
132
175
|
this.resonantInstance._queueUpdate(this.variableName, 'filtered');
|
|
@@ -142,12 +185,28 @@ class Resonant {
|
|
|
142
185
|
this.arrayDataChangeDetection = {};
|
|
143
186
|
}
|
|
144
187
|
|
|
188
|
+
_handleInputElement(element, value, onChangeCallback) {
|
|
189
|
+
if (element.type === 'checkbox') {
|
|
190
|
+
element.checked = value;
|
|
191
|
+
if (!element.hasAttribute('data-resonant-bound')) {
|
|
192
|
+
element.onchange = () => onChangeCallback(element.checked);
|
|
193
|
+
element.setAttribute('data-resonant-bound', 'true');
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
element.value = value;
|
|
197
|
+
if (!element.hasAttribute('data-resonant-bound')) {
|
|
198
|
+
element.oninput = () => onChangeCallback(element.value);
|
|
199
|
+
element.setAttribute('data-resonant-bound', 'true');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
145
204
|
add(variableName, value, persist) {
|
|
146
205
|
value = this.persist(variableName, value, persist);
|
|
147
206
|
if (Array.isArray(value)) {
|
|
148
207
|
this.data[variableName] = new ObservableArray(variableName, this, ...value);
|
|
149
208
|
this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
|
|
150
|
-
} else if (typeof value === 'object') {
|
|
209
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
151
210
|
this.data[variableName] = this._createObject(variableName, value);
|
|
152
211
|
} else {
|
|
153
212
|
this.data[variableName] = value;
|
|
@@ -162,8 +221,8 @@ class Resonant {
|
|
|
162
221
|
return value;
|
|
163
222
|
}
|
|
164
223
|
var found = localStorage.getItem('res_' + variableName);
|
|
165
|
-
if (found !== null && found !== undefined){
|
|
166
|
-
return JSON.parse(
|
|
224
|
+
if (found !== null && found !== undefined) {
|
|
225
|
+
return JSON.parse(found);
|
|
167
226
|
} else {
|
|
168
227
|
localStorage.setItem('res_' + variableName, JSON.stringify(value));
|
|
169
228
|
return value;
|
|
@@ -182,6 +241,10 @@ class Resonant {
|
|
|
182
241
|
});
|
|
183
242
|
}
|
|
184
243
|
|
|
244
|
+
_resolveValue(instance, key, override = null) {
|
|
245
|
+
return override ?? instance[key];
|
|
246
|
+
}
|
|
247
|
+
|
|
185
248
|
_createObject(variableName, obj) {
|
|
186
249
|
obj[Symbol('isProxy')] = true;
|
|
187
250
|
return new Proxy(obj, {
|
|
@@ -196,15 +259,56 @@ class Resonant {
|
|
|
196
259
|
});
|
|
197
260
|
}
|
|
198
261
|
|
|
262
|
+
_evaluateDisplayCondition(element, item, condition) {
|
|
263
|
+
try {
|
|
264
|
+
const show = new Function('item', `return ${condition}`)(item);
|
|
265
|
+
element.style.display = show ? '' : 'none';
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.error(`Error evaluating display condition: ${condition}`, e);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_handleDisplayElements(parentElement, instance) {
|
|
272
|
+
const displayElements = parentElement.querySelectorAll('[res-display]');
|
|
273
|
+
displayElements.forEach(displayEl => {
|
|
274
|
+
const condition = displayEl.getAttribute('res-display') || '';
|
|
275
|
+
this._evaluateDisplayCondition(displayEl, instance, condition);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
_bindClickEvents(parentElement, instance, arrayValue) {
|
|
280
|
+
const onclickElements = parentElement.querySelectorAll('[res-onclick], [res-onclick-remove]');
|
|
281
|
+
onclickElements.forEach(onclickEl => {
|
|
282
|
+
const functionName = onclickEl.getAttribute('res-onclick');
|
|
283
|
+
const removeKey = onclickEl.getAttribute('res-onclick-remove');
|
|
284
|
+
|
|
285
|
+
if (functionName) {
|
|
286
|
+
onclickEl.onclick = () => {
|
|
287
|
+
const func = new Function('item', `return ${functionName}(item)`);
|
|
288
|
+
func(instance);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (removeKey) {
|
|
292
|
+
onclickEl.onclick = () => {
|
|
293
|
+
if (arrayValue) {
|
|
294
|
+
const removeIdx = arrayValue.findIndex(t => t[removeKey] === instance[removeKey]);
|
|
295
|
+
if (removeIdx !== -1) {
|
|
296
|
+
arrayValue.splice(removeIdx, 1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
199
304
|
_defineProperty(variableName) {
|
|
200
305
|
Object.defineProperty(window, variableName, {
|
|
201
306
|
get: () => this.data[variableName],
|
|
202
307
|
set: (newValue) => {
|
|
203
308
|
if (Array.isArray(newValue)) {
|
|
204
309
|
this.data[variableName] = new ObservableArray(variableName, this, ...newValue);
|
|
205
|
-
this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
|
|
206
|
-
|
|
207
|
-
} else if (typeof newValue === 'object') {
|
|
310
|
+
this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
|
|
311
|
+
} else if (typeof newValue === 'object' && newValue !== null) {
|
|
208
312
|
this.data[variableName] = this._createObject(variableName, newValue);
|
|
209
313
|
} else {
|
|
210
314
|
this.data[variableName] = newValue;
|
|
@@ -212,7 +316,8 @@ class Resonant {
|
|
|
212
316
|
this.updateElement(variableName);
|
|
213
317
|
this.updateDisplayConditionalsFor(variableName);
|
|
214
318
|
this.updateStylesFor(variableName);
|
|
215
|
-
|
|
319
|
+
|
|
320
|
+
if (!Array.isArray(newValue) && (typeof newValue !== 'object' || newValue === null)) {
|
|
216
321
|
this._queueUpdate(variableName, 'modified', this.data[variableName]);
|
|
217
322
|
}
|
|
218
323
|
}
|
|
@@ -223,7 +328,6 @@ class Resonant {
|
|
|
223
328
|
if (!this.pendingUpdates.has(variableName)) {
|
|
224
329
|
this.pendingUpdates.set(variableName, []);
|
|
225
330
|
}
|
|
226
|
-
|
|
227
331
|
this.pendingUpdates.get(variableName).push({ action, item, property, oldValue });
|
|
228
332
|
|
|
229
333
|
if (this.pendingUpdates.get(variableName).length === 1) {
|
|
@@ -232,16 +336,19 @@ class Resonant {
|
|
|
232
336
|
this.updatePersistantData(variableName);
|
|
233
337
|
this.pendingUpdates.delete(variableName);
|
|
234
338
|
|
|
235
|
-
updates = updates.filter((v, i, a) =>
|
|
339
|
+
updates = updates.filter((v, i, a) =>
|
|
340
|
+
a.findIndex(t => (t.property === v.property && t.action === v.action)) === i
|
|
341
|
+
);
|
|
342
|
+
|
|
236
343
|
updates.forEach(update => {
|
|
237
344
|
this._triggerCallbacks(variableName, update);
|
|
238
345
|
});
|
|
346
|
+
|
|
239
347
|
this.updateElement(variableName);
|
|
240
348
|
this.updateDisplayConditionalsFor(variableName);
|
|
241
349
|
this.updateStylesFor(variableName);
|
|
242
350
|
}, 0);
|
|
243
351
|
}
|
|
244
|
-
|
|
245
352
|
}
|
|
246
353
|
|
|
247
354
|
_triggerCallbacks(variableName, callbackData) {
|
|
@@ -258,55 +365,27 @@ class Resonant {
|
|
|
258
365
|
const value = this.data[variableName];
|
|
259
366
|
|
|
260
367
|
elements.forEach(element => {
|
|
261
|
-
element.value = value;
|
|
262
368
|
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
} else if (Array.isArray(value)) {
|
|
271
|
-
element.querySelectorAll(`[res="${variableName}"][res-rendered=true]`).forEach(el => el.remove());
|
|
369
|
+
this._handleInputElement(element, value, (newValue) => {
|
|
370
|
+
this.data[variableName] = newValue;
|
|
371
|
+
this._queueUpdate(variableName, 'modified', this.data[variableName]);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else if (Array.isArray(value)) {
|
|
375
|
+
element.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(el => el.remove());
|
|
272
376
|
this._renderArray(variableName, element);
|
|
273
|
-
}
|
|
274
|
-
|
|
377
|
+
}
|
|
378
|
+
else if (typeof value === 'object' && value !== null) {
|
|
379
|
+
const subElements = element.querySelectorAll('[res-prop]');
|
|
275
380
|
subElements.forEach(subEl => {
|
|
276
381
|
const key = subEl.getAttribute('res-prop');
|
|
277
382
|
if (key && key in value) {
|
|
278
|
-
|
|
279
|
-
if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
|
|
280
|
-
if (subEl.type === 'checkbox') {
|
|
281
|
-
subEl.checked = value[key];
|
|
282
|
-
subEl.onchange = () => {
|
|
283
|
-
this.data[variableName][key] = subEl.checked;
|
|
284
|
-
};
|
|
285
|
-
} else {
|
|
286
|
-
subEl.value = value[key];
|
|
287
|
-
subEl.oninput = () => {
|
|
288
|
-
this.data[variableName][key] = subEl.value;
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
subEl.innerHTML = value[key];
|
|
293
|
-
}
|
|
294
|
-
subEl.setAttribute('data-resonant-bound', 'true');
|
|
295
|
-
} else {
|
|
296
|
-
if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
|
|
297
|
-
if (subEl.type === 'checkbox') {
|
|
298
|
-
subEl.checked = value[key];
|
|
299
|
-
} else {
|
|
300
|
-
subEl.value = value[key];
|
|
301
|
-
}
|
|
302
|
-
} else {
|
|
303
|
-
subEl.innerHTML = value[key];
|
|
304
|
-
}
|
|
305
|
-
}
|
|
383
|
+
this._renderObjectProperty(subEl, value[key], variableName, key);
|
|
306
384
|
}
|
|
307
385
|
});
|
|
308
|
-
}
|
|
309
|
-
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
element.innerHTML = value ?? '';
|
|
310
389
|
}
|
|
311
390
|
});
|
|
312
391
|
|
|
@@ -314,25 +393,151 @@ class Resonant {
|
|
|
314
393
|
this.updateStylesFor(variableName);
|
|
315
394
|
}
|
|
316
395
|
|
|
396
|
+
_renderObjectProperty(subEl, propValue, parentVarName, key) {
|
|
397
|
+
if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
|
|
398
|
+
!Array.isArray(propValue) &&
|
|
399
|
+
typeof propValue !== 'object') {
|
|
400
|
+
this._handleInputElement(subEl, propValue, (newValue) => {
|
|
401
|
+
window[parentVarName][key] = newValue;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else if (Array.isArray(propValue)) {
|
|
405
|
+
this._renderNestedArray(subEl, propValue);
|
|
406
|
+
}
|
|
407
|
+
else if (typeof propValue === 'object' && propValue !== null) {
|
|
408
|
+
const nestedElements = subEl.querySelectorAll('[res-prop]');
|
|
409
|
+
nestedElements.forEach(nestedEl => {
|
|
410
|
+
const nestedKey = nestedEl.getAttribute('res-prop');
|
|
411
|
+
if (nestedKey && nestedKey in propValue) {
|
|
412
|
+
this._renderObjectProperty(nestedEl, propValue[nestedKey], parentVarName, nestedKey);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
subEl.innerHTML = propValue ?? '';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_renderArray(variableName, el) {
|
|
422
|
+
const container = el.hasAttribute('res') && el.parentElement ? el.parentElement : el;
|
|
423
|
+
|
|
424
|
+
let template;
|
|
425
|
+
if (!window[variableName + "_template"]) {
|
|
426
|
+
template = el.cloneNode(true);
|
|
427
|
+
window[variableName + "_template"] = template;
|
|
428
|
+
el.style.display = 'none';
|
|
429
|
+
el.setAttribute('res-template', 'true');
|
|
430
|
+
} else {
|
|
431
|
+
template = window[variableName + "_template"];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const existingElements = new Map();
|
|
435
|
+
container.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(element => {
|
|
436
|
+
const key = element.getAttribute('res-key');
|
|
437
|
+
if (key) {
|
|
438
|
+
existingElements.set(key, element);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
container.querySelectorAll(`[res="${variableName}"][res-rendered="true"]`).forEach(el => el.remove());
|
|
443
|
+
|
|
444
|
+
this.data[variableName].forEach((instance, index) => {
|
|
445
|
+
const elementKey = instance.key;
|
|
446
|
+
let elementToUse;
|
|
447
|
+
|
|
448
|
+
if (existingElements.has(elementKey)) {
|
|
449
|
+
elementToUse = existingElements.get(elementKey);
|
|
450
|
+
existingElements.delete(elementKey);
|
|
451
|
+
elementToUse.setAttribute("res-index", index);
|
|
452
|
+
} else {
|
|
453
|
+
elementToUse = template.cloneNode(true);
|
|
454
|
+
elementToUse.removeAttribute('res-template');
|
|
455
|
+
elementToUse.style.display = '';
|
|
456
|
+
elementToUse.setAttribute("res-rendered", "true");
|
|
457
|
+
elementToUse.setAttribute("res-key", elementKey);
|
|
458
|
+
elementToUse.setAttribute("res-index", index);
|
|
459
|
+
|
|
460
|
+
for (let key in instance) {
|
|
461
|
+
let overrideInstanceValue = null;
|
|
462
|
+
let subEl = elementToUse.querySelector(`[res-prop="${key}"]`);
|
|
463
|
+
if (!subEl) {
|
|
464
|
+
subEl = elementToUse.querySelector('[res-prop=""]');
|
|
465
|
+
overrideInstanceValue = instance;
|
|
466
|
+
}
|
|
467
|
+
if (subEl) {
|
|
468
|
+
const value = this._resolveValue(instance, key, overrideInstanceValue);
|
|
469
|
+
|
|
470
|
+
if ((subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') &&
|
|
471
|
+
!Array.isArray(value) &&
|
|
472
|
+
typeof value !== 'object') {
|
|
473
|
+
this._handleInputElement(
|
|
474
|
+
subEl,
|
|
475
|
+
value,
|
|
476
|
+
(newValue) => {
|
|
477
|
+
instance[key] = newValue;
|
|
478
|
+
this._queueUpdate(variableName, 'modified', instance, key, value);
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
else if (Array.isArray(value)) {
|
|
483
|
+
this._renderNestedArray(subEl, value);
|
|
484
|
+
}
|
|
485
|
+
else if (typeof value === 'object' && value !== null) {
|
|
486
|
+
const nestedElements = subEl.querySelectorAll('[res-prop]');
|
|
487
|
+
nestedElements.forEach(nestedEl => {
|
|
488
|
+
const nestedKey = nestedEl.getAttribute('res-prop');
|
|
489
|
+
if (nestedKey && nestedKey in value) {
|
|
490
|
+
this._renderObjectProperty(nestedEl, value[nestedKey], variableName, nestedKey);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
subEl.innerHTML = value ?? '';
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
this._handleDisplayElements(elementToUse, instance);
|
|
502
|
+
this._bindClickEvents(elementToUse, instance, this.data[variableName]);
|
|
503
|
+
|
|
504
|
+
container.appendChild(elementToUse);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
_renderNestedArray(subEl, arrayValue) {
|
|
508
|
+
const template = subEl.cloneNode(true);
|
|
509
|
+
subEl.innerHTML = '';
|
|
510
|
+
|
|
511
|
+
subEl.removeAttribute('res-prop');
|
|
512
|
+
|
|
513
|
+
arrayValue.forEach((item, idx) => {
|
|
514
|
+
const cloned = template.cloneNode(true);
|
|
515
|
+
cloned.setAttribute('res-rendered', 'true');
|
|
516
|
+
|
|
517
|
+
const nestedEls = cloned.querySelectorAll('[res-prop]');
|
|
518
|
+
nestedEls.forEach(nestedEl => {
|
|
519
|
+
const nestedKey = nestedEl.getAttribute('res-prop');
|
|
520
|
+
if (nestedKey && nestedKey in item) {
|
|
521
|
+
this._renderObjectProperty(nestedEl, item[nestedKey], null, nestedKey);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
this._handleDisplayElements(cloned, item);
|
|
526
|
+
this._bindClickEvents(cloned, item, arrayValue);
|
|
527
|
+
|
|
528
|
+
subEl.appendChild(cloned);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
317
531
|
updateDisplayConditionalsFor(variableName) {
|
|
318
532
|
const conditionalElements = document.querySelectorAll(`[res-display*="${variableName}"]`);
|
|
319
533
|
conditionalElements.forEach(conditionalElement => {
|
|
320
534
|
const condition = conditionalElement.getAttribute('res-display');
|
|
321
|
-
|
|
322
|
-
if (eval(condition)) {
|
|
323
|
-
conditionalElement.style.display = '';
|
|
324
|
-
} else {
|
|
325
|
-
conditionalElement.style.display = 'none';
|
|
326
|
-
}
|
|
327
|
-
} catch (e) {
|
|
328
|
-
console.error(`Error evaluating condition for ${variableName}: ${condition}`, e);
|
|
329
|
-
}
|
|
535
|
+
this._evaluateDisplayCondition(conditionalElement, this.data[variableName], condition);
|
|
330
536
|
});
|
|
331
537
|
}
|
|
332
538
|
|
|
333
539
|
updateStylesFor(variableName) {
|
|
334
540
|
const styleElements = document.querySelectorAll(`[res-style*="${variableName}"]`);
|
|
335
|
-
|
|
336
541
|
styleElements.forEach(styleElement => {
|
|
337
542
|
let styleCondition = styleElement.getAttribute('res-style');
|
|
338
543
|
try {
|
|
@@ -355,23 +560,21 @@ class Resonant {
|
|
|
355
560
|
const item = this.data[variableName][index];
|
|
356
561
|
styleCondition = styleCondition.replace(new RegExp(`\\b${variableName}\\b`, 'g'), 'item');
|
|
357
562
|
const styleClass = new Function('item', `return ${styleCondition}`)(item);
|
|
358
|
-
|
|
359
563
|
if (styleClass) {
|
|
360
564
|
styleElement.classList.add(styleClass);
|
|
361
565
|
} else {
|
|
362
|
-
|
|
566
|
+
const elementHasStyle = styleElement.classList.contains(styleClass);
|
|
363
567
|
if (elementHasStyle) {
|
|
364
568
|
styleElement.classList.remove(styleClass);
|
|
365
569
|
}
|
|
366
570
|
}
|
|
367
571
|
} else {
|
|
368
572
|
const styleClass = eval(styleCondition);
|
|
369
|
-
|
|
370
573
|
if (styleClass) {
|
|
371
574
|
styleElement.classList.add(styleClass);
|
|
372
575
|
styleElement.setAttribute('res-styles', styleClass);
|
|
373
576
|
} else {
|
|
374
|
-
|
|
577
|
+
const elementHasStyle = styleElement.classList.contains(styleClass);
|
|
375
578
|
if (elementHasStyle) {
|
|
376
579
|
styleElement.classList.remove(styleClass);
|
|
377
580
|
}
|
|
@@ -383,86 +586,6 @@ class Resonant {
|
|
|
383
586
|
});
|
|
384
587
|
}
|
|
385
588
|
|
|
386
|
-
_renderArray(variableName, el) {
|
|
387
|
-
let template = el.cloneNode(true);
|
|
388
|
-
el.innerHTML = '';
|
|
389
|
-
|
|
390
|
-
if (!window[variableName + "_template"]) {
|
|
391
|
-
window[variableName + "_template"] = template;
|
|
392
|
-
} else {
|
|
393
|
-
template = window[variableName + "_template"];
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
this.data[variableName].forEach((instance, index) => {
|
|
397
|
-
const clonedEl = template.cloneNode(true);
|
|
398
|
-
clonedEl.setAttribute("res-index", index);
|
|
399
|
-
for (let key in instance) {
|
|
400
|
-
let overrideInstanceValue = null;
|
|
401
|
-
let subEl = clonedEl.querySelector(`[res-prop="${key}"]`);
|
|
402
|
-
if(!subEl) {
|
|
403
|
-
subEl = clonedEl.querySelector(`[res-prop=""]`);
|
|
404
|
-
overrideInstanceValue = instance;
|
|
405
|
-
}
|
|
406
|
-
if (subEl) {
|
|
407
|
-
if (!subEl.hasAttribute('data-resonant-bound')) {
|
|
408
|
-
if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
|
|
409
|
-
if (subEl.type === 'checkbox') {
|
|
410
|
-
subEl.checked = overrideInstanceValue ?? instance[key] ;
|
|
411
|
-
subEl.onchange = () => {
|
|
412
|
-
instance[key] = subEl.checked;
|
|
413
|
-
this._queueUpdate(variableName, 'modified', instance, key, overrideInstanceValue ?? instance[key]);
|
|
414
|
-
};
|
|
415
|
-
} else {
|
|
416
|
-
subEl.value = overrideInstanceValue ?? instance[key];
|
|
417
|
-
subEl.oninput = () => {
|
|
418
|
-
instance[key] = subEl.value;
|
|
419
|
-
this._queueUpdate(variableName, 'modified', instance, key, overrideInstanceValue ?? instance[key]);
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
423
|
-
subEl.innerHTML = overrideInstanceValue ?? instance[key];
|
|
424
|
-
}
|
|
425
|
-
subEl.setAttribute('data-resonant-bound', 'true');
|
|
426
|
-
} else {
|
|
427
|
-
if (subEl.tagName === 'INPUT' || subEl.tagName === 'TEXTAREA') {
|
|
428
|
-
if (subEl.type === 'checkbox') {
|
|
429
|
-
subEl.checked = overrideInstanceValue ?? instance[key];
|
|
430
|
-
} else {
|
|
431
|
-
subEl.value = overrideInstanceValue ?? instance[key];
|
|
432
|
-
}
|
|
433
|
-
} else {
|
|
434
|
-
subEl.innerHTML = overrideInstanceValue ?? instance[key];
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const onclickElements = clonedEl.querySelectorAll('[res-onclick], [res-onclick-remove]');
|
|
441
|
-
onclickElements.forEach(onclickEl => {
|
|
442
|
-
const functionName = onclickEl.getAttribute('res-onclick');
|
|
443
|
-
const removeKey = onclickEl.getAttribute('res-onclick-remove');
|
|
444
|
-
if (functionName) {
|
|
445
|
-
onclickEl.onclick = () => {
|
|
446
|
-
const func = new Function('item', `return ${functionName}(item)`);
|
|
447
|
-
func(instance);
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (removeKey) {
|
|
452
|
-
onclickEl.onclick = () => {
|
|
453
|
-
const index = this.data[variableName].findIndex(t => t[removeKey] === instance[removeKey]);
|
|
454
|
-
if (index !== -1) {
|
|
455
|
-
this.data[variableName].splice(index, 1);
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
clonedEl.setAttribute("res-rendered", true);
|
|
462
|
-
el.appendChild(clonedEl);
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
589
|
addCallback(variableName, method) {
|
|
467
590
|
if (!this.callbacks[variableName]) {
|
|
468
591
|
this.callbacks[variableName] = [];
|
package/resonant.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class ObservableArray extends Array{constructor(e,t,...a){if(void 0===t)return super(...a);super(...a);var s=void 0===t.data[e];this.variableName=e,this.resonantInstance=t,this.isDeleting=!1,this.forEach(((e,t)=>{"object"==typeof e&&(this[t]=this._createProxy(e,t))})),s||this.forceUpdate()}_createProxy(e,t){return new Proxy(e,{set:(e,a,s)=>{if(e[a]!==s){const i=e[a];e[a]=s,this.resonantInstance._queueUpdate(this.variableName,"modified",e,a,i,t)}return!0}})}forceUpdate(){this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"modified",this.slice())}update(e){window[this.variableName]=e,this.resonantInstance._queueUpdate(this.variableName,"updated",e)}push(...e){e=e.map(((e,t)=>"object"==typeof e?this._createProxy(e,this.length+t):e));const t=super.push(...e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),e.forEach(((e,t)=>{this.resonantInstance._queueUpdate(this.variableName,"added",e,this.length-1-t)})),t}splice(e,t,...a){a=a.map(((t,a)=>"object"==typeof t?this._createProxy(t,e+a):t));const s=super.splice(e,t,...a);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),t>0&&s.forEach(((t,a)=>{this.resonantInstance._queueUpdate(this.variableName,"removed",t,e+a)})),a.length>0&&a.forEach(((t,a)=>{this.resonantInstance._queueUpdate(this.variableName,"added",t,e+a)})),s}set(e,t){if(this[e]!==t){if(this.isDeleting)return!0;const a=this.resonantInstance.arrayDataChangeDetection[this.variableName];let s="modified";(e>=a.length||void 0===a[e])&&(s="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();const i=this[e];this[e]=t,this.resonantInstance._queueUpdate(this.variableName,s,this[e],e,i)}return!0}delete(e){const t=this[e];return this.isDeleting=!0,this.splice(e,1),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"removed",null,e,t),this.isDeleting=!1,!0}filter(e,t=!0){if(void 0===this.resonantInstance||!1===t)return super.filter(e);const a=super.filter(e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"filtered"),a}}class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Map,this.arrayDataChangeDetection={}}add(e,t,a){t=this.persist(e,t,a),Array.isArray(t)?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):this.data[e]="object"==typeof t?this._createObject(e,t):t,this._defineProperty(e),this.updateElement(e)}persist(e,t,a){if(void 0===a||!a)return t;var s=localStorage.getItem("res_"+e);return null!=s?JSON.parse(localStorage.getItem("res_"+e)):(localStorage.setItem("res_"+e,JSON.stringify(t)),t)}updatePersistantData(e){localStorage.getItem("res_"+e)&&localStorage.setItem("res_"+e,JSON.stringify(this.data[e]))}addAll(e){Object.entries(e).forEach((([e,t])=>{this.add(e,t)}))}_createObject(e,t){return t[Symbol("isProxy")]=!0,new Proxy(t,{set:(t,a,s)=>{if(t[a]!==s){const i=t[a];t[a]=s,this._queueUpdate(e,"modified",t,a,i)}return!0}})}_defineProperty(e){Object.defineProperty(window,e,{get:()=>this.data[e],set:t=>{Array.isArray(t)?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):this.data[e]="object"==typeof t?this._createObject(e,t):t,this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e),Array.isArray(t)||"object"==typeof t||this._queueUpdate(e,"modified",this.data[e])}})}_queueUpdate(e,t,a,s,i){this.pendingUpdates.has(e)||this.pendingUpdates.set(e,[]),this.pendingUpdates.get(e).push({action:t,item:a,property:s,oldValue:i}),1===this.pendingUpdates.get(e).length&&setTimeout((()=>{let t=this.pendingUpdates.get(e);this.updatePersistantData(e),this.pendingUpdates.delete(e),t=t.filter(((e,t,a)=>a.findIndex((t=>t.property===e.property&&t.action===e.action))===t)),t.forEach((t=>{this._triggerCallbacks(e,t)})),this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}),0)}_triggerCallbacks(e,t){this.callbacks[e]&&this.callbacks[e].forEach((a=>{const s=t.item||t.oldValue;a(this.data[e],s,t.action)}))}updateElement(e){const t=document.querySelectorAll(`[res="${e}"]`),a=this.data[e];t.forEach((t=>{if(t.value=a,"INPUT"===t.tagName||"TEXTAREA"===t.tagName)t.hasAttribute("data-resonant-bound")||(t.oninput=()=>{this.data[e]=t.value,this._queueUpdate(e,"modified",this.data[e])},t.setAttribute("data-resonant-bound","true"));else if(Array.isArray(a))t.querySelectorAll(`[res="${e}"][res-rendered=true]`).forEach((e=>e.remove())),this._renderArray(e,t);else if("object"==typeof a){t.querySelectorAll("[res-prop]").forEach((t=>{const s=t.getAttribute("res-prop");s&&s in a&&(t.hasAttribute("data-resonant-bound")?"INPUT"===t.tagName||"TEXTAREA"===t.tagName?"checkbox"===t.type?t.checked=a[s]:t.value=a[s]:t.innerHTML=a[s]:("INPUT"===t.tagName||"TEXTAREA"===t.tagName?"checkbox"===t.type?(t.checked=a[s],t.onchange=()=>{this.data[e][s]=t.checked}):(t.value=a[s],t.oninput=()=>{this.data[e][s]=t.value}):t.innerHTML=a[s],t.setAttribute("data-resonant-bound","true")))}))}else t.innerHTML=a})),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}updateDisplayConditionalsFor(variableName){const conditionalElements=document.querySelectorAll(`[res-display*="${variableName}"]`);conditionalElements.forEach((conditionalElement=>{const condition=conditionalElement.getAttribute("res-display");try{eval(condition)?conditionalElement.style.display="":conditionalElement.style.display="none"}catch(e){console.error(`Error evaluating condition for ${variableName}: ${condition}`,e)}}))}updateStylesFor(variableName){const styleElements=document.querySelectorAll(`[res-style*="${variableName}"]`);styleElements.forEach((styleElement=>{let styleCondition=styleElement.getAttribute("res-style");try{let parent=styleElement,index=null;for(;parent&&!index;)index=parent.getAttribute("res-index"),parent=parent.parentElement;let resStyles=styleElement.getAttribute("res-styles");if(resStyles){let e=resStyles.split(" ");e.forEach((e=>{styleElement.classList.remove(e)}))}if(null!==index){const e=this.data[variableName][index];styleCondition=styleCondition.replace(new RegExp(`\\b${variableName}\\b`,"g"),"item");const t=new Function("item",`return ${styleCondition}`)(e);if(t)styleElement.classList.add(t);else{var elementHasStyle=styleElement.classList.contains(t);elementHasStyle&&styleElement.classList.remove(t)}}else{const styleClass=eval(styleCondition);if(styleClass)styleElement.classList.add(styleClass),styleElement.setAttribute("res-styles",styleClass);else{var elementHasStyle=styleElement.classList.contains(styleClass);elementHasStyle&&styleElement.classList.remove(styleClass)}}}catch(e){console.error(`Error evaluating style for ${variableName}: ${styleCondition}`,e)}}))}_renderArray(e,t){let a=t.cloneNode(!0);t.innerHTML="",window[e+"_template"]?a=window[e+"_template"]:window[e+"_template"]=a,this.data[e].forEach(((s,i)=>{const n=a.cloneNode(!0);n.setAttribute("res-index",i);for(let t in s){let a=null,i=n.querySelector(`[res-prop="${t}"]`);i||(i=n.querySelector('[res-prop=""]'),a=s),i&&(i.hasAttribute("data-resonant-bound")?"INPUT"===i.tagName||"TEXTAREA"===i.tagName?"checkbox"===i.type?i.checked=a??s[t]:i.value=a??s[t]:i.innerHTML=a??s[t]:("INPUT"===i.tagName||"TEXTAREA"===i.tagName?"checkbox"===i.type?(i.checked=a??s[t],i.onchange=()=>{s[t]=i.checked,this._queueUpdate(e,"modified",s,t,a??s[t])}):(i.value=a??s[t],i.oninput=()=>{s[t]=i.value,this._queueUpdate(e,"modified",s,t,a??s[t])}):i.innerHTML=a??s[t],i.setAttribute("data-resonant-bound","true")))}n.querySelectorAll("[res-onclick], [res-onclick-remove]").forEach((t=>{const a=t.getAttribute("res-onclick"),i=t.getAttribute("res-onclick-remove");a&&(t.onclick=()=>{new Function("item",`return ${a}(item)`)(s)}),i&&(t.onclick=()=>{const t=this.data[e].findIndex((e=>e[i]===s[i]));-1!==t&&this.data[e].splice(t,1)})})),n.setAttribute("res-rendered",!0),t.appendChild(n)}))}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}
|
|
1
|
+
class ObservableArray extends Array{constructor(e,t,...r){if(void 0===t)return super(...r);super(...r);var s=void 0===t.data[e];this.variableName=e,this.resonantInstance=t,this.isDeleting=!1,this.forEach((e,t)=>{"object"==typeof e&&(this._setKeyForObjectAndChildObjects(e,t),this[t]=this._createProxy(e,t))}),s||this.forceUpdate()}_setKeyForObjectAndChildObjects(e,t){"object"==typeof e&&(e.key=t+"-"+this._generateKeyByContentCheckSum(e),Object.keys(e).forEach(r=>{this._setKeyForObjectAndChildObjects(e[r],t)}))}_generateKeyByContentCheckSum(e){let t=e=>{if(!e||"object"!=typeof e)return e;if(Array.isArray(e))return e.map(e=>t(e));let r={...e};return delete r.key,Object.keys(r).forEach(e=>{"object"==typeof r[e]&&null!==r[e]&&(r[e]=t(r[e]))}),r},r=t(e),s=JSON.stringify(r),a=0;for(let i=0;i<s.length;i++){let n=s.charCodeAt(i);a=(a<<5)-a+n,a&=a}return Math.abs(a).toString(36)}_createProxy(e,t){return new Proxy(e,{set:(r,s,a)=>{if(r[s]!==a){let i=r[s];r[s]=a,this._setKeyForObjectAndChildObjects(e,t),this.resonantInstance._queueUpdate(this.variableName,"modified",r,s,i,t)}return!0}})}forceUpdate(){this.forEach((e,t)=>{console.log("forceUpdate",e,t),this._setKeyForObjectAndChildObjects(e,t)}),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"modified",this.slice())}update(e){window[this.variableName]=e,this.resonantInstance._queueUpdate(this.variableName,"updated",e)}push(...e){e=e.map((e,t)=>"object"==typeof e?(this._setKeyForObjectAndChildObjects(e,this.length+t),this._createProxy(e,this.length+t)):e);let t=super.push(...e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),e.forEach((e,t)=>{this.resonantInstance._queueUpdate(this.variableName,"added",e,this.length-1-t)}),t}splice(e,t,...r){r=r.map((t,r)=>"object"==typeof t?this._createProxy(t,e+r):t);let s=super.splice(e,t,...r);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),t>0&&s.forEach((t,r)=>{this.resonantInstance._queueUpdate(this.variableName,"removed",t,e+r)}),r.length>0&&r.forEach((t,r)=>{this.resonantInstance._queueUpdate(this.variableName,"added",t,e+r)}),s}set(e,t){if(this[e]!==t){if(this.isDeleting)return!0;let r=this.resonantInstance.arrayDataChangeDetection[this.variableName],s="modified";e>=r.length?s="added":void 0===r[e]&&(s="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();let a=this[e];this[e]=t,this.resonantInstance._queueUpdate(this.variableName,s,this[e],e,a)}return!0}delete(e){let t=this[e];return this.isDeleting=!0,this.splice(e,1),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"removed",null,e,t),this.isDeleting=!1,!0}filter(e,t=!0){if(void 0===this.resonantInstance||!1===t)return super.filter(e);let r=super.filter(e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"filtered"),r}}class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Map,this.arrayDataChangeDetection={}}_handleInputElement(e,t,r){"checkbox"===e.type?(e.checked=t,e.hasAttribute("data-resonant-bound")||(e.onchange=()=>r(e.checked),e.setAttribute("data-resonant-bound","true"))):(e.value=t,e.hasAttribute("data-resonant-bound")||(e.oninput=()=>r(e.value),e.setAttribute("data-resonant-bound","true")))}add(e,t,r){Array.isArray(t=this.persist(e,t,r))?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):"object"==typeof t&&null!==t?this.data[e]=this._createObject(e,t):this.data[e]=t,this._defineProperty(e),this.updateElement(e)}persist(e,t,r){if(void 0===r||!r)return t;var s=localStorage.getItem("res_"+e);return null!=s?JSON.parse(s):(localStorage.setItem("res_"+e,JSON.stringify(t)),t)}updatePersistantData(e){localStorage.getItem("res_"+e)&&localStorage.setItem("res_"+e,JSON.stringify(this.data[e]))}addAll(e){Object.entries(e).forEach(([e,t])=>{this.add(e,t)})}_resolveValue(e,t,r=null){return r??e[t]}_createObject(e,t){return t[Symbol("isProxy")]=!0,new Proxy(t,{set:(t,r,s)=>{if(t[r]!==s){let a=t[r];t[r]=s,this._queueUpdate(e,"modified",t,r,a)}return!0}})}_evaluateDisplayCondition(e,t,r){try{let s=Function("item",`return ${r}`)(t);e.style.display=s?"":"none"}catch(a){console.error(`Error evaluating display condition: ${r}`,a)}}_handleDisplayElements(e,t){let r=e.querySelectorAll("[res-display]");r.forEach(e=>{let r=e.getAttribute("res-display")||"";this._evaluateDisplayCondition(e,t,r)})}_bindClickEvents(e,t,r){let s=e.querySelectorAll("[res-onclick], [res-onclick-remove]");s.forEach(e=>{let s=e.getAttribute("res-onclick"),a=e.getAttribute("res-onclick-remove");s&&(e.onclick=()=>{let e=Function("item",`return ${s}(item)`);e(t)}),a&&(e.onclick=()=>{if(r){let e=r.findIndex(e=>e[a]===t[a]);-1!==e&&r.splice(e,1)}})})}_defineProperty(e){Object.defineProperty(window,e,{get:()=>this.data[e],set:t=>{Array.isArray(t)?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):"object"==typeof t&&null!==t?this.data[e]=this._createObject(e,t):this.data[e]=t,this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e),Array.isArray(t)||"object"==typeof t&&null!==t||this._queueUpdate(e,"modified",this.data[e])}})}_queueUpdate(e,t,r,s,a){this.pendingUpdates.has(e)||this.pendingUpdates.set(e,[]),this.pendingUpdates.get(e).push({action:t,item:r,property:s,oldValue:a}),1===this.pendingUpdates.get(e).length&&setTimeout(()=>{let t=this.pendingUpdates.get(e);this.updatePersistantData(e),this.pendingUpdates.delete(e),(t=t.filter((e,t,r)=>r.findIndex(t=>t.property===e.property&&t.action===e.action)===t)).forEach(t=>{this._triggerCallbacks(e,t)}),this.updateElement(e),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)},0)}_triggerCallbacks(e,t){this.callbacks[e]&&this.callbacks[e].forEach(r=>{let s=t.item||t.oldValue;r(this.data[e],s,t.action)})}updateElement(e){let t=document.querySelectorAll(`[res="${e}"]`),r=this.data[e];t.forEach(t=>{if("INPUT"===t.tagName||"TEXTAREA"===t.tagName)this._handleInputElement(t,r,t=>{this.data[e]=t,this._queueUpdate(e,"modified",this.data[e])});else if(Array.isArray(r))t.querySelectorAll(`[res="${e}"][res-rendered="true"]`).forEach(e=>e.remove()),this._renderArray(e,t);else if("object"==typeof r&&null!==r){let s=t.querySelectorAll("[res-prop]");s.forEach(t=>{let s=t.getAttribute("res-prop");s&&s in r&&this._renderObjectProperty(t,r[s],e,s)})}else t.innerHTML=r??""}),this.updateDisplayConditionalsFor(e),this.updateStylesFor(e)}_renderObjectProperty(e,t,r,s){if("INPUT"!==e.tagName&&"TEXTAREA"!==e.tagName||Array.isArray(t)||"object"==typeof t){if(Array.isArray(t))this._renderNestedArray(e,t);else if("object"==typeof t&&null!==t){let a=e.querySelectorAll("[res-prop]");a.forEach(e=>{let s=e.getAttribute("res-prop");s&&s in t&&this._renderObjectProperty(e,t[s],r,s)})}else e.innerHTML=t??""}else this._handleInputElement(e,t,e=>{window[r][s]=e})}_renderArray(e,t){let r=t.hasAttribute("res")&&t.parentElement?t.parentElement:t,s;window[e+"_template"]?s=window[e+"_template"]:(s=t.cloneNode(!0),window[e+"_template"]=s,t.style.display="none",t.setAttribute("res-template","true"));let a=new Map;r.querySelectorAll(`[res="${e}"][res-rendered="true"]`).forEach(e=>{let t=e.getAttribute("res-key");t&&a.set(t,e)}),r.querySelectorAll(`[res="${e}"][res-rendered="true"]`).forEach(e=>e.remove()),this.data[e].forEach((t,i)=>{let n=t.key,l;if(a.has(n))l=a.get(n),a.delete(n),l.setAttribute("res-index",i);else for(let o in(l=s.cloneNode(!0)).removeAttribute("res-template"),l.style.display="",l.setAttribute("res-rendered","true"),l.setAttribute("res-key",n),l.setAttribute("res-index",i),t){let h=null,d=l.querySelector(`[res-prop="${o}"]`);if(d||(d=l.querySelector('[res-prop=""]'),h=t),d){let c=this._resolveValue(t,o,h);if("INPUT"!==d.tagName&&"TEXTAREA"!==d.tagName||Array.isArray(c)||"object"==typeof c){if(Array.isArray(c))this._renderNestedArray(d,c);else if("object"==typeof c&&null!==c){let u=d.querySelectorAll("[res-prop]");u.forEach(t=>{let r=t.getAttribute("res-prop");r&&r in c&&this._renderObjectProperty(t,c[r],e,r)})}else d.innerHTML=c??""}else this._handleInputElement(d,c,r=>{t[o]=r,this._queueUpdate(e,"modified",t,o,c)})}}this._handleDisplayElements(l,t),this._bindClickEvents(l,t,this.data[e]),r.appendChild(l)})}_renderNestedArray(e,t){let r=e.cloneNode(!0);e.innerHTML="",e.removeAttribute("res-prop"),t.forEach((s,a)=>{let i=r.cloneNode(!0);i.setAttribute("res-rendered","true");let n=i.querySelectorAll("[res-prop]");n.forEach(e=>{let t=e.getAttribute("res-prop");t&&t in s&&this._renderObjectProperty(e,s[t],null,t)}),this._handleDisplayElements(i,s),this._bindClickEvents(i,s,t),e.appendChild(i)})}updateDisplayConditionalsFor(e){let t=document.querySelectorAll(`[res-display*="${e}"]`);t.forEach(t=>{let r=t.getAttribute("res-display");this._evaluateDisplayCondition(t,this.data[e],r)})}updateStylesFor(variableName){let styleElements=document.querySelectorAll(`[res-style*="${variableName}"]`);styleElements.forEach(styleElement=>{let styleCondition=styleElement.getAttribute("res-style");try{let parent=styleElement,index=null;for(;parent&&!index;)index=parent.getAttribute("res-index"),parent=parent.parentElement;let resStyles=styleElement.getAttribute("res-styles");if(resStyles&&resStyles.split(" ").forEach(e=>{styleElement.classList.remove(e)}),null!==index){let item=this.data[variableName][index];styleCondition=styleCondition.replace(RegExp(`\\b${variableName}\\b`,"g"),"item");let styleClass=Function("item",`return ${styleCondition}`)(item);if(styleClass)styleElement.classList.add(styleClass);else{let elementHasStyle=styleElement.classList.contains(styleClass);elementHasStyle&&styleElement.classList.remove(styleClass)}}else{let styleClass=eval(styleCondition);if(styleClass)styleElement.classList.add(styleClass),styleElement.setAttribute("res-styles",styleClass);else{let elementHasStyle=styleElement.classList.contains(styleClass);elementHasStyle&&styleElement.classList.remove(styleClass)}}}catch(e){console.error(`Error evaluating style for ${variableName}: ${styleCondition}`,e)}})}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}
|