resonantjs 1.0.5 → 1.0.7

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/Demo.gif CHANGED
Binary file
package/README.md CHANGED
@@ -9,7 +9,6 @@ Resonant.js is an open-source lightweight JavaScript framework that enables reac
9
9
  - **Bidirectional Input Binding**: Bind HTML input fields directly to your data model.
10
10
  - **Efficient Conditional Updates**: Only evaluate conditional expressions tied to specific variable changes.
11
11
  - **Lightweight and Easy to Integrate**: Minimal setup required to get started.
12
- - **Compatible with Modern Browsers**: Works seamlessly across all modern web browsers.
13
12
  ## Installation
14
13
  ## NPM
15
14
  To install via NPM, use the following command:
@@ -60,96 +59,20 @@ Include resonant.js in your HTML file, and use the following example to understa
60
59
  - **`res` and `res-prop` Attributes**: Bind HTML elements to your data model seamlessly.
61
60
  - `res` is used to identify an overarching data model.
62
61
  - `res-prop` links individual properties within that model to corresponding UI elements.
63
- - **`res-conditional` Attribute**: Conditionally display elements based on the data model's properties.
62
+ - **`res-display` Attribute**: Conditionally display elements based on the data model's properties.
64
63
  - **`res-onclick` Attribute**: Triggers a function when an element is clicked, allowing for custom event handling.
65
64
  - **`res-onclick-remove` Attribute**: Removes an item from an array when the associated element is clicked.
65
+ - **`res-style` Attribute**: Dynamically update CSS styles based on data model properties.
66
66
  - **Automatic UI Updates**: Changes to your JavaScript objects instantly reflect in the associated UI components, reducing manual DOM manipulation.
67
67
 
68
68
  ### Advanced Features
69
69
  - **Dynamic Arrays and Objects**: Easily handle collections and nested objects to dynamically add or remove elements based on your data structures.
70
70
  - **Event Callbacks**: Register custom functions to execute whenever your data model changes.
71
71
  - **Bidirectional Input Binding**: Bind form input fields directly to your data, making two-way synchronization simple.
72
+ - **Optional Persistent Data**: Save your data model to local storage for easy retrieval and persistence across sessions.
72
73
 
73
- ### New Features in Version 1.0.2
74
-
75
- #### Pending Updates Mechanism
76
- - Introduced to prevent redundant updates and ensure callbacks are only triggered once per update cycle, improving performance and user experience.
77
-
78
- #### Callback Parameter Enhancement
79
- - Callbacks now receive detailed parameters including the specific action taken (`added`, `modified`, `removed`), the item affected, and the previous value. This provides better context for handling updates.
80
-
81
- #### Batched Updates for Object Properties
82
- - Improved handling of object property updates to ensure changes are batched together, preventing multiple redundant callback triggers.
83
-
84
- #### Refined Data Binding
85
- - Enhanced data binding between model and view to ensure consistent synchronization without unnecessary updates.
86
-
87
- ### Task Manager Example
88
-
89
- Here is an example of a task manager application using Resonant.js:
90
-
91
- ```html
92
- <!DOCTYPE html>
93
- <html lang="en">
94
- <head>
95
- <title>Resonant.js Task Manager Demo</title>
96
- <script src="https://cdn.jsdelivr.net/npm/resonantjs@latest/dist/resonant.min.js"></script>
97
- </head>
98
- <body>
99
- <h1>Resonant.js Task Manager Demo</h1>
100
-
101
- <!-- Task Input -->
102
- <div>
103
- <h2>Add New Task</h2>
104
- <input type="text" placeholder="Task Name" res="taskName" />
105
- <button onclick="addTask()">Add Task</button>
106
- </div>
107
-
108
- <!-- Task List -->
109
- <div>
110
- <h2>Task List</h2>
111
- <ul res="tasks">
112
- <li>
113
- <input type="checkbox" res-prop="done" />
114
- <span res-prop="name"></span>
115
- <button res-onclick-remove="name">Remove</button>
116
- </li>
117
- </ul>
118
- </div>
119
-
120
- <script>
121
- const resonantJs = new Resonant();
122
-
123
- // Initialize variables using a configuration object
124
- resonantJs.addAll({
125
- tasks: [
126
- { name: "Task 1", done: false },
127
- { name: "Task 2", done: true }
128
- ],
129
- taskName: ""
130
- });
131
-
132
- // Add a callback to log actions taken on tasks
133
- resonantJs.addCallback("tasks", (tasks, task, action) => {
134
- console.log(`Action taken: ${action} for ${task.name}`);
135
- });
136
-
137
- // Add a function to add a new task
138
- function addTask() {
139
- const newTask = { name: taskName, done: false };
140
- tasks.push(newTask);
141
- taskName = '';
142
- }
143
- </script>
144
- </body>
145
- </html>
146
- ```
147
-
148
- ## Future Enhancements
149
-
150
- - [ ] Support for nested data structures with deep linking.
151
- - [ ] Advanced filtering and sorting methods for arrays.
152
- - [ ] Compatibility with popular JavaScript frameworks.
74
+ ## Other Information
75
+ - The demo HTML file uses the Pico CSS framework for styling. You can find more information about Pico CSS [here](https://picocss.com/).
153
76
 
154
77
  ## License
155
78
 
@@ -2,52 +2,85 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <title>Resonant.js Practical Demo</title>
5
- <script src="../resonant.js"></script>
5
+ <link
6
+ rel="stylesheet"
7
+ href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.min.css"
8
+ />
9
+ <script src="https://unpkg.com/resonantjs@latest/resonant.min.js"></script>
10
+ <style>
11
+ section.grid {
12
+ padding: 1rem;
13
+ margin-bottom: 2rem;
14
+ border: 1px solid #e0e0e0;
15
+ border-radius: 0.5rem;
16
+ }
17
+ .conditional {
18
+ font-style: italic;
19
+ color: #888;
20
+ }
21
+ </style>
6
22
  </head>
7
23
  <body>
8
- <h1>Resonant.js Practical Demo</h1>
9
24
 
10
- <!-- Display and update a single item -->
11
- <div>
12
- <h2>Counter</h2>
13
- <p>
14
- Current count: <span res="counter"></span>
15
- </p>
16
- <div res-conditional="counter < 10">
17
- Only shows when counter is less than 10
18
- </div>
19
- <div res-conditional="counter >= 10">
20
- Only shows when counter is greater than or equal to 10
21
- </div>
22
- <button onclick="counter++">Increment Counter</button>
23
- </div>
25
+ <main class="container">
26
+ <h1>Resonant.js Basic Demo</h1>
24
27
 
25
- <!-- Demonstrate object property binding -->
26
- <div>
27
- <h2>User Information</h2>
28
- <div res="user">
29
- <span res-prop="firstname"></span>
30
- <span res-prop="lastname"></span>
31
- <br/>
32
- <div res-conditional="user.firstname == 'John' && user.lastname == 'Doe'">
33
- Only shows when firstname is John and lastname is Doe
28
+ <!-- Display and update a single item -->
29
+ <section class="grid">
30
+ <div>
31
+ <h2>Counter: <span res="counter"></span></h2>
32
+ <div res-display="counter < 10" class="conditional">
33
+ Only shows when counter is less than 10
34
+ </div>
35
+ <div res-display="counter >= 10" class="conditional">
36
+ Only shows when counter is greater than or equal to 10
37
+ </div>
34
38
  </div>
35
- <br/>
36
- First Name: <input type="text" res-prop="firstname" />
37
- Last Name: <input type="text" res-prop="lastname" />
38
- </div>
39
- </div>
40
39
 
41
- <!-- Demonstrate dynamic list rendering -->
42
- <div>
43
- <h2>Project Members</h2>
44
- <ul res="projectTeam">
45
- <li>
46
- <span res-prop="name"></span> - <span res-prop="role"></span>
47
- </li>
48
- </ul>
49
- <button onclick="addProjectMember()">Add Project Member</button>
50
- </div>
40
+ <button onclick="counter++">Increment Counter</button>
41
+ </section>
42
+
43
+ <!-- Demonstrate object property binding -->
44
+ <section class="grid" res="user">
45
+ <div>
46
+ <h2>User Information</h2>
47
+ <div res-display="user.firstname == 'John' && user.lastname == 'Doe'" class="conditional">
48
+ Only shows when firstname is John and lastname is Doe
49
+ </div>
50
+
51
+ <p>
52
+ <span res-prop="firstname"></span>
53
+ <span res-prop="lastname"></span>
54
+ </p>
55
+ </div>
56
+
57
+ <div>
58
+
59
+ <div class="form-group">
60
+ <label>First Name:
61
+ <input type="text" res-prop="firstname" />
62
+ </label>
63
+ <label>Last Name:
64
+ <input type="text" res-prop="lastname" />
65
+ </label>
66
+ </div>
67
+ </div>
68
+ </section>
69
+
70
+ <!-- Demonstrate dynamic list rendering -->
71
+ <section class="grid">
72
+ <div>
73
+ <h2>Project Members</h2>
74
+ <ul res="projectTeam">
75
+ <li>
76
+ <span res-prop="name"></span> - <span res-prop="role"></span>
77
+ </li>
78
+ </ul>
79
+ </div>
80
+
81
+ <button onclick="addProjectMember()">Add Project Member</button>
82
+ </section>
83
+ </main>
51
84
 
52
85
  <script>
53
86
  const resonantJs = new Resonant();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resonantjs",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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,5 +1,8 @@
1
1
  class ObservableArray extends Array {
2
2
  constructor(variableName, resonantInstance, ...args) {
3
+ if(resonantInstance === undefined) {
4
+ return super(...args);
5
+ }
3
6
  super(...args);
4
7
  this.variableName = variableName;
5
8
  this.resonantInstance = resonantInstance;
@@ -69,6 +72,17 @@ class ObservableArray extends Array {
69
72
  this.isDeleting = false;
70
73
  return true;
71
74
  }
75
+
76
+ filter(filter) {
77
+ if(this.resonantInstance === undefined) {
78
+ return super.filter(filter);
79
+ }
80
+
81
+ const result = super.filter(filter);
82
+ this.resonantInstance.arrayDataChangeDetection[this.variableName] = this.slice();
83
+ this.resonantInstance._queueUpdate(this.variableName, 'filtered');
84
+ return result;
85
+ }
72
86
  }
73
87
 
74
88
  class Resonant {
@@ -76,10 +90,11 @@ class Resonant {
76
90
  this.data = {};
77
91
  this.callbacks = {};
78
92
  this.pendingUpdates = new Map();
79
- this.arrayDataChangeDetection = {}; // Added to keep track of array state
93
+ this.arrayDataChangeDetection = {};
80
94
  }
81
95
 
82
- add(variableName, value) {
96
+ add(variableName, value, persist) {
97
+ value = this.persist(variableName, value, persist);
83
98
  if (Array.isArray(value)) {
84
99
  this.data[variableName] = new ObservableArray(variableName, this, ...value);
85
100
  this.arrayDataChangeDetection[variableName] = this.data[variableName].slice();
@@ -93,6 +108,25 @@ class Resonant {
93
108
  this.updateElement(variableName);
94
109
  }
95
110
 
111
+ persist(variableName, value, persist) {
112
+ if (persist === undefined || !persist) {
113
+ return value;
114
+ }
115
+ var found = localStorage.getItem('res_' + variableName);
116
+ if (found !== null && found !== undefined){
117
+ return JSON.parse(localStorage.getItem('res_' + variableName));
118
+ } else {
119
+ localStorage.setItem('res_' + variableName, JSON.stringify(value));
120
+ return value;
121
+ }
122
+ }
123
+
124
+ updatePersistentData(variableName) {
125
+ if (localStorage.getItem('res_' + variableName)) {
126
+ localStorage.setItem('res_' + variableName, JSON.stringify(this.data[variableName]));
127
+ }
128
+ }
129
+
96
130
  addAll(config) {
97
131
  Object.entries(config).forEach(([variableName, value]) => {
98
132
  this.add(variableName, value);
@@ -127,7 +161,7 @@ class Resonant {
127
161
  this.data[variableName] = newValue;
128
162
  }
129
163
  this.updateElement(variableName);
130
- this.updateConditionalsFor(variableName);
164
+ this.updateDisplayConditionalsFor(variableName);
131
165
  this.updateStylesFor(variableName);
132
166
  if (!Array.isArray(newValue) && typeof newValue !== 'object') {
133
167
  this._queueUpdate(variableName, 'modified', this.data[variableName]);
@@ -146,12 +180,13 @@ class Resonant {
146
180
  if (this.pendingUpdates.get(variableName).length === 1) {
147
181
  setTimeout(() => {
148
182
  const updates = this.pendingUpdates.get(variableName);
183
+ this.updatePersistentData(variableName);
149
184
  this.pendingUpdates.delete(variableName);
150
185
  updates.forEach(update => {
151
186
  this._triggerCallbacks(variableName, update);
152
187
  });
153
188
  this.updateElement(variableName);
154
- this.updateConditionalsFor(variableName);
189
+ this.updateDisplayConditionalsFor(variableName);
155
190
  this.updateStylesFor(variableName);
156
191
  }, 0);
157
192
  }
@@ -223,14 +258,14 @@ class Resonant {
223
258
  }
224
259
  });
225
260
 
226
- this.updateConditionalsFor(variableName);
261
+ this.updateDisplayConditionalsFor(variableName);
227
262
  this.updateStylesFor(variableName);
228
263
  }
229
264
 
230
- updateConditionalsFor(variableName) {
231
- const conditionalElements = document.querySelectorAll(`[res-conditional*="${variableName}"]`);
265
+ updateDisplayConditionalsFor(variableName) {
266
+ const conditionalElements = document.querySelectorAll(`[res-display*="${variableName}"]`);
232
267
  conditionalElements.forEach(conditionalElement => {
233
- const condition = conditionalElement.getAttribute('res-conditional');
268
+ const condition = conditionalElement.getAttribute('res-display');
234
269
  try {
235
270
  if (eval(condition)) {
236
271
  conditionalElement.style.display = '';
@@ -256,6 +291,14 @@ class Resonant {
256
291
  parent = parent.parentElement;
257
292
  }
258
293
 
294
+ let resStyles = styleElement.getAttribute('res-styles');
295
+ if (resStyles) {
296
+ let resStylesArray = resStyles.split(' ');
297
+ resStylesArray.forEach(resStyle => {
298
+ styleElement.classList.remove(resStyle);
299
+ });
300
+ }
301
+
259
302
  if (index !== null) {
260
303
  const item = this.data[variableName][index];
261
304
  styleCondition = styleCondition.replace(new RegExp(`\\b${variableName}\\b`, 'g'), 'item');
@@ -271,8 +314,10 @@ class Resonant {
271
314
  }
272
315
  } else {
273
316
  const styleClass = eval(styleCondition);
317
+
274
318
  if (styleClass) {
275
319
  styleElement.classList.add(styleClass);
320
+ styleElement.setAttribute('res-styles', styleClass);
276
321
  } else {
277
322
  var elementHasStyle = styleElement.classList.contains(styleClass);
278
323
  if (elementHasStyle) {
@@ -339,7 +384,6 @@ class Resonant {
339
384
  onclickElements.forEach(onclickEl => {
340
385
  const functionName = onclickEl.getAttribute('res-onclick');
341
386
  const removeKey = onclickEl.getAttribute('res-onclick-remove');
342
-
343
387
  if (functionName) {
344
388
  onclickEl.onclick = () => {
345
389
  const func = new Function('item', `return ${functionName}(item)`);
@@ -368,4 +412,4 @@ class Resonant {
368
412
  }
369
413
  this.callbacks[variableName].push(method);
370
414
  }
371
- }
415
+ }
package/resonant.min.js CHANGED
@@ -1 +1 @@
1
- class ObservableArray extends Array{constructor(e,t,...a){super(...a),this.variableName=e,this.resonantInstance=t,this.isDeleting=!1}push(...e){var 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(a,e,...t){var s=super.splice(a,e,...t);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),0<e&&s.forEach((e,t)=>{this.resonantInstance._queueUpdate(this.variableName,"removed",e,a+t)}),0<t.length&&t.forEach((e,t)=>{this.resonantInstance._queueUpdate(this.variableName,"added",e,a+t)}),s}set(t,a){if(this[t]!==a){if(this.isDeleting)return!0;var s=this.resonantInstance.arrayDataChangeDetection[this.variableName];let e="modified";(t>=s.length||void 0===s[t])&&(e="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();s=this[t];this[t]=a,this.resonantInstance._queueUpdate(this.variableName,e,this[t],t,s)}return!0}delete(e){var 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)}}class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Map,this.arrayDataChangeDetection={}}add(e,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._defineProperty(e),this.updateElement(e)}addAll(e){Object.entries(e).forEach(([e,t])=>{this.add(e,t)})}_createObject(i,e){return e[Symbol("isProxy")]=!0,new Proxy(e,{set:(e,t,a)=>{var s;return e[t]!==a&&(s=e[t],e[t]=a,this._queueUpdate(i,"modified",e,t,s)),!0}})}_defineProperty(t){Object.defineProperty(window,t,{get:()=>this.data[t],set:e=>{Array.isArray(e)?(this.data[t]=new ObservableArray(t,this,...e),this.arrayDataChangeDetection[t]=this.data[t].slice()):this.data[t]="object"==typeof e?this._createObject(t,e):e,this.updateElement(t),this.updateConditionalsFor(t),this.updateStylesFor(t),Array.isArray(e)||"object"==typeof e||this._queueUpdate(t,"modified",this.data[t])}})}_queueUpdate(t,e,a,s,i){this.pendingUpdates.has(t)||this.pendingUpdates.set(t,[]),this.pendingUpdates.get(t).push({action:e,item:a,property:s,oldValue:i}),1===this.pendingUpdates.get(t).length&&setTimeout(()=>{var e=this.pendingUpdates.get(t);this.pendingUpdates.delete(t),e.forEach(e=>{this._triggerCallbacks(t,e)}),this.updateElement(t),this.updateConditionalsFor(t),this.updateStylesFor(t)},0)}_triggerCallbacks(a,s){this.callbacks[a]&&this.callbacks[a].forEach(e=>{var t=s.item||s.oldValue;e(this.data[a],t,s.action)})}updateElement(a){var e=document.querySelectorAll(`[res="${a}"]`);const s=this.data[a];e.forEach(e=>{e.value=s,"INPUT"===e.tagName||"TEXTAREA"===e.tagName?e.hasAttribute("data-resonant-bound")||(e.oninput=()=>{this.data[a]=e.value,this._queueUpdate(a,"modified",this.data[a])},e.setAttribute("data-resonant-bound","true")):Array.isArray(s)?(e.querySelectorAll(`[res="${a}"][res-rendered=true]`).forEach(e=>e.remove()),this._renderArray(a,e)):"object"==typeof s?e.querySelectorAll("[res-prop]").forEach(e=>{const t=e.getAttribute("res-prop");t&&t in s&&(e.hasAttribute("data-resonant-bound")?"INPUT"===e.tagName||"TEXTAREA"===e.tagName?"checkbox"===e.type?e.checked=s[t]:e.value=s[t]:e.innerHTML=s[t]:("INPUT"===e.tagName||"TEXTAREA"===e.tagName?"checkbox"===e.type?(e.checked=s[t],e.onchange=()=>{this.data[a][t]=e.checked}):(e.value=s[t],e.oninput=()=>{this.data[a][t]=e.value}):e.innerHTML=s[t],e.setAttribute("data-resonant-bound","true")))}):e.innerHTML=s}),this.updateConditionalsFor(a),this.updateStylesFor(a)}updateConditionalsFor(variableName){const conditionalElements=document.querySelectorAll(`[res-conditional*="${variableName}"]`);conditionalElements.forEach(conditionalElement=>{const condition=conditionalElement.getAttribute("res-conditional");try{eval(condition)?conditionalElement.style.display="":conditionalElement.style.display="none"}catch(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;if(null!==index){const item=this.data[variableName][index],styleClass=(styleCondition=styleCondition.replace(new RegExp(`\\b${variableName}\\b`,"g"),"item"),new Function("item","return "+styleCondition)(item));var elementHasStyle;styleClass?styleElement.classList.add(styleClass):(elementHasStyle=styleElement.classList.contains(styleClass),elementHasStyle&&styleElement.classList.remove(styleClass))}else{const styleClass=eval(styleCondition);var elementHasStyle;styleClass?styleElement.classList.add(styleClass):(elementHasStyle=styleElement.classList.contains(styleClass),elementHasStyle&&styleElement.classList.remove(styleClass))}}catch(e){}})}_renderArray(i,n){let r=n.cloneNode(!0);n.innerHTML="",window[i+"_template"]?r=window[i+"_template"]:window[i+"_template"]=r,this.data[i].forEach((s,e)=>{var t=r.cloneNode(!0);t.setAttribute("res-index",e);for(let e in s){const a=t.querySelector(`[res-prop="${e}"]`);a&&(a.hasAttribute("data-resonant-bound")?"INPUT"===a.tagName||"TEXTAREA"===a.tagName?"checkbox"===a.type?a.checked=s[e]:a.value=s[e]:a.innerHTML=s[e]:("INPUT"===a.tagName||"TEXTAREA"===a.tagName?"checkbox"===a.type?(a.checked=s[e],a.onchange=()=>{s[e]=a.checked,this._queueUpdate(i,"modified",s,e,s[e])}):(a.value=s[e],a.oninput=()=>{s[e]=a.value,this._queueUpdate(i,"modified",s,e,s[e])}):a.innerHTML=s[e],a.setAttribute("data-resonant-bound","true")))}t.querySelectorAll("[res-onclick], [res-onclick-remove]").forEach(e=>{const t=e.getAttribute("res-onclick"),a=e.getAttribute("res-onclick-remove");t&&(e.onclick=()=>{new Function("item",`return ${t}(item)`)(s)}),a&&(e.onclick=()=>{var e=this.data[i].findIndex(e=>e[a]===s[a]);-1!==e&&this.data[i].splice(e,1)})}),t.setAttribute("res-rendered",!0),n.appendChild(t)})}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}
1
+ class ObservableArray extends Array{constructor(e,t,...a){if(void 0===t)return super(...a);super(...a),this.variableName=e,this.resonantInstance=t,this.isDeleting=!1}push(...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,...a){let 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;let a=this.resonantInstance.arrayDataChangeDetection[this.variableName],s="modified";e>=a.length?s="added":void 0===a[e]&&(s="added"),this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice();let i=this[e];this[e]=t,this.resonantInstance._queueUpdate(this.variableName,s,this[e],e,i)}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){if(void 0===this.resonantInstance)return super.filter(e);let t=super.filter(e);return this.resonantInstance.arrayDataChangeDetection[this.variableName]=this.slice(),this.resonantInstance._queueUpdate(this.variableName,"filtered"),t}}class Resonant{constructor(){this.data={},this.callbacks={},this.pendingUpdates=new Map,this.arrayDataChangeDetection={}}add(e,t,a){Array.isArray(t=this.persist(e,t,a))?(this.data[e]=new ObservableArray(e,this,...t),this.arrayDataChangeDetection[e]=this.data[e].slice()):"object"==typeof t?this.data[e]=this._createObject(e,t):this.data[e]=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)}updatePersistentData(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){let 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()):"object"==typeof 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||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.updatePersistentData(e),this.pendingUpdates.delete(e),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=>{let s=t.item||t.oldValue;a(this.data[e],s,t.action)})}updateElement(e){let 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){let s=t.querySelectorAll("[res-prop]");s.forEach(t=>{let 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){let conditionalElements=document.querySelectorAll(`[res-display*="${variableName}"]`);conditionalElements.forEach(conditionalElement=>{let 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){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{var 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{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)=>{let r=a.cloneNode(!0);for(let n in r.setAttribute("res-index",i),s){let l=r.querySelector(`[res-prop="${n}"]`);l&&(l.hasAttribute("data-resonant-bound")?"INPUT"===l.tagName||"TEXTAREA"===l.tagName?"checkbox"===l.type?l.checked=s[n]:l.value=s[n]:l.innerHTML=s[n]:("INPUT"===l.tagName||"TEXTAREA"===l.tagName?"checkbox"===l.type?(l.checked=s[n],l.onchange=()=>{s[n]=l.checked,this._queueUpdate(e,"modified",s,n,s[n])}):(l.value=s[n],l.oninput=()=>{s[n]=l.value,this._queueUpdate(e,"modified",s,n,s[n])}):l.innerHTML=s[n],l.setAttribute("data-resonant-bound","true")))}let h=r.querySelectorAll("[res-onclick], [res-onclick-remove]");h.forEach(t=>{let a=t.getAttribute("res-onclick"),i=t.getAttribute("res-onclick-remove");a&&(t.onclick=()=>{let e=Function("item",`return ${a}(item)`);e(s)}),i&&(t.onclick=()=>{let t=this.data[e].findIndex(e=>e[i]===s[i]);-1!==t&&this.data[e].splice(t,1)})}),r.setAttribute("res-rendered",!0),t.appendChild(r)})}addCallback(e,t){this.callbacks[e]||(this.callbacks[e]=[]),this.callbacks[e].push(t)}}