@ape-egg/vibe 1.0.2 → 1.0.5
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/CHANGELOG.md +32 -0
- package/ROADMAP.md +289 -0
- package/_vibe-compiled-iteration-batch.js +9 -8
- package/affected.js +47 -25
- package/conditionals.js +15 -24
- package/constants.js +103 -22
- package/hydrate.js +7 -19
- package/index.js +43 -32
- package/iterate.js +54 -31
- package/iteration-utils.js +17 -17
- package/link.js +1 -1
- package/package.json +1 -1
- package/parse.js +39 -32
- package/state.js +5 -5
- package/utils.js +16 -4
package/constants.js
CHANGED
|
@@ -1,56 +1,137 @@
|
|
|
1
1
|
// Elements that should not have reactive bindings
|
|
2
|
-
export const NON_REACTIVE_ELEMENTS = [
|
|
2
|
+
export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
|
|
3
3
|
|
|
4
4
|
// Attributes where the string value is meaningful (should NOT be removed when falsy)
|
|
5
5
|
// All other attributes are treated as boolean-like (removed when falsy, present when truthy)
|
|
6
6
|
export const VALUE_ATTRS = [
|
|
7
7
|
// Global attributes
|
|
8
|
-
'class',
|
|
9
|
-
'
|
|
8
|
+
'class',
|
|
9
|
+
'style',
|
|
10
|
+
'id',
|
|
11
|
+
'title',
|
|
12
|
+
'lang',
|
|
13
|
+
'dir',
|
|
14
|
+
'tabindex',
|
|
15
|
+
'accesskey',
|
|
16
|
+
'slot',
|
|
17
|
+
'part',
|
|
18
|
+
'is',
|
|
19
|
+
'nonce',
|
|
20
|
+
'popover',
|
|
21
|
+
'anchor',
|
|
10
22
|
|
|
11
23
|
// Enumerated (take specific string values, not truly boolean)
|
|
12
|
-
'contenteditable',
|
|
13
|
-
'
|
|
24
|
+
'contenteditable',
|
|
25
|
+
'draggable',
|
|
26
|
+
'spellcheck',
|
|
27
|
+
'translate',
|
|
28
|
+
'autocapitalize',
|
|
29
|
+
'inputmode',
|
|
30
|
+
'enterkeyhint',
|
|
31
|
+
'virtualkeyboardpolicy',
|
|
14
32
|
|
|
15
33
|
// URLs and sources
|
|
16
|
-
'href',
|
|
17
|
-
'
|
|
34
|
+
'href',
|
|
35
|
+
'src',
|
|
36
|
+
'action',
|
|
37
|
+
'cite',
|
|
38
|
+
'data',
|
|
39
|
+
'poster',
|
|
40
|
+
'srcset',
|
|
41
|
+
'imagesrcset',
|
|
42
|
+
'formaction',
|
|
43
|
+
'ping',
|
|
44
|
+
'usemap',
|
|
45
|
+
'manifest',
|
|
46
|
+
'codebase',
|
|
18
47
|
|
|
19
48
|
// Form attributes
|
|
20
|
-
'name',
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
49
|
+
'name',
|
|
50
|
+
'type',
|
|
51
|
+
'value',
|
|
52
|
+
'placeholder',
|
|
53
|
+
'pattern',
|
|
54
|
+
'min',
|
|
55
|
+
'max',
|
|
56
|
+
'step',
|
|
57
|
+
'minlength',
|
|
58
|
+
'maxlength',
|
|
59
|
+
'size',
|
|
60
|
+
'accept',
|
|
61
|
+
'autocomplete',
|
|
62
|
+
'list',
|
|
63
|
+
'form',
|
|
64
|
+
'formmethod',
|
|
65
|
+
'formtarget',
|
|
66
|
+
'formenctype',
|
|
67
|
+
'wrap',
|
|
68
|
+
'method',
|
|
69
|
+
'enctype',
|
|
70
|
+
'for',
|
|
71
|
+
'dirname',
|
|
24
72
|
|
|
25
73
|
// Text/accessibility
|
|
26
|
-
'alt',
|
|
74
|
+
'alt',
|
|
75
|
+
'label',
|
|
76
|
+
'summary',
|
|
77
|
+
'abbr',
|
|
27
78
|
|
|
28
79
|
// Dimensions and layout
|
|
29
|
-
'width',
|
|
30
|
-
'
|
|
80
|
+
'width',
|
|
81
|
+
'height',
|
|
82
|
+
'cols',
|
|
83
|
+
'rows',
|
|
84
|
+
'span',
|
|
85
|
+
'rowspan',
|
|
86
|
+
'colspan',
|
|
87
|
+
'low',
|
|
88
|
+
'high',
|
|
89
|
+
'optimum',
|
|
31
90
|
|
|
32
91
|
// Link/resource hints
|
|
33
|
-
'target',
|
|
34
|
-
'
|
|
35
|
-
'
|
|
92
|
+
'target',
|
|
93
|
+
'rel',
|
|
94
|
+
'hreflang',
|
|
95
|
+
'download',
|
|
96
|
+
'as',
|
|
97
|
+
'media',
|
|
98
|
+
'type',
|
|
99
|
+
'charset',
|
|
100
|
+
'crossorigin',
|
|
101
|
+
'integrity',
|
|
102
|
+
'loading',
|
|
103
|
+
'decoding',
|
|
104
|
+
'fetchpriority',
|
|
105
|
+
'referrerpolicy',
|
|
106
|
+
'blocking',
|
|
107
|
+
'imagesizes',
|
|
108
|
+
'sizes',
|
|
36
109
|
|
|
37
110
|
// Media
|
|
38
|
-
'preload',
|
|
111
|
+
'preload',
|
|
112
|
+
'kind',
|
|
113
|
+
'srclang',
|
|
39
114
|
|
|
40
115
|
// Meta
|
|
41
|
-
'content',
|
|
116
|
+
'content',
|
|
117
|
+
'http-equiv',
|
|
42
118
|
|
|
43
119
|
// iframe/embed
|
|
44
|
-
'sandbox',
|
|
120
|
+
'sandbox',
|
|
121
|
+
'allow',
|
|
122
|
+
'srcdoc',
|
|
123
|
+
'credentialless',
|
|
45
124
|
|
|
46
125
|
// Table
|
|
47
|
-
'headers',
|
|
126
|
+
'headers',
|
|
127
|
+
'scope',
|
|
48
128
|
|
|
49
129
|
// Datetime
|
|
50
130
|
'datetime',
|
|
51
131
|
|
|
52
132
|
// Object/embed legacy
|
|
53
|
-
'coords',
|
|
133
|
+
'coords',
|
|
134
|
+
'shape',
|
|
54
135
|
];
|
|
55
136
|
|
|
56
137
|
// Properties that should be set directly on the DOM element (not as attributes)
|
package/hydrate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { updateIteration } from './iterate.js'
|
|
2
|
-
import { updateConditional } from './conditionals.js'
|
|
3
|
-
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js'
|
|
1
|
+
import { updateIteration } from './iterate.js';
|
|
2
|
+
import { updateConditional } from './conditionals.js';
|
|
3
|
+
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
|
|
4
|
+
import { evalInScope } from './utils.js';
|
|
4
5
|
|
|
5
6
|
// Keep track of old state for diffing
|
|
6
7
|
let previousState = {};
|
|
@@ -9,19 +10,6 @@ export const setPreviousState = (state) => {
|
|
|
9
10
|
previousState = { ...state };
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
// Evaluate expression in the context of state
|
|
13
|
-
const evalInScope = (expr, state) => {
|
|
14
|
-
try {
|
|
15
|
-
const keys = Object.keys(state);
|
|
16
|
-
const values = Object.values(state);
|
|
17
|
-
// Create a function with state keys as parameters and evaluate the expression
|
|
18
|
-
return new Function(...keys, `'use strict'; return (${expr})`)(...values);
|
|
19
|
-
} catch (e) {
|
|
20
|
-
// Fallback to undefined if evaluation fails
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
13
|
export default (affected, state, linkList = {}) => {
|
|
26
14
|
affected.forEach((aff) => {
|
|
27
15
|
// Handle iteration updates
|
|
@@ -40,12 +28,12 @@ export default (affected, state, linkList = {}) => {
|
|
|
40
28
|
if (aff.type === 'attribute') {
|
|
41
29
|
const { attrName, attrValue, element } = aff;
|
|
42
30
|
try {
|
|
43
|
-
|
|
44
31
|
// Check if this is a pure binding (e.g., value="@[inputValue]")
|
|
45
32
|
const isPureBinding = attrValue.match(PURE_BINDING_REGEX);
|
|
46
33
|
const isDomProperty = DOM_PROPERTIES.includes(attrName);
|
|
47
34
|
// Value attrs keep their string value; everything else is boolean-like (removed when falsy)
|
|
48
|
-
const isValueAttr =
|
|
35
|
+
const isValueAttr =
|
|
36
|
+
VALUE_ATTRS.includes(attrName) ||
|
|
49
37
|
attrName.startsWith('data-') ||
|
|
50
38
|
attrName.startsWith('aria-') ||
|
|
51
39
|
attrName.startsWith('on');
|
|
@@ -97,4 +85,4 @@ export default (affected, state, linkList = {}) => {
|
|
|
97
85
|
|
|
98
86
|
// Update previous state for next diff
|
|
99
87
|
setPreviousState(state);
|
|
100
|
-
}
|
|
88
|
+
};
|
package/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import state from
|
|
2
|
-
import parse from
|
|
3
|
-
import link from
|
|
4
|
-
import hydrate, { setPreviousState } from
|
|
5
|
-
import affected from
|
|
6
|
-
import { deepMerge, hash } from
|
|
7
|
-
import { renderAllIterations, setRenderAllConditionals } from
|
|
8
|
-
import { renderAllConditionals } from
|
|
9
|
-
import { NON_REACTIVE_ELEMENTS } from
|
|
1
|
+
import state from './state.js';
|
|
2
|
+
import parse from './parse.js';
|
|
3
|
+
import link from './link.js';
|
|
4
|
+
import hydrate, { setPreviousState } from './hydrate.js';
|
|
5
|
+
import affected from './affected.js';
|
|
6
|
+
import { deepMerge, hash } from './utils.js';
|
|
7
|
+
import { renderAllIterations, setRenderAllConditionals } from './iterate.js';
|
|
8
|
+
import { renderAllConditionals } from './conditionals.js';
|
|
9
|
+
import { NON_REACTIVE_ELEMENTS } from './constants.js';
|
|
10
10
|
|
|
11
11
|
// Wire up cross-module dependency after all modules are loaded
|
|
12
12
|
setRenderAllConditionals(renderAllConditionals);
|
|
@@ -29,12 +29,12 @@ const isNonReactiveOrInside = (node) => {
|
|
|
29
29
|
// Navigate tree using dot notation (handles .children at each level)
|
|
30
30
|
const navigateTree = (tree, path) => {
|
|
31
31
|
if (!path) return tree;
|
|
32
|
-
return path.split(
|
|
32
|
+
return path.split('.').reduce((node, key) => node?.children?.[key], tree);
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// Get or create a node in the tree at the given path
|
|
36
36
|
const ensureNode = (tree, path) => {
|
|
37
|
-
const keys = path.split(
|
|
37
|
+
const keys = path.split('.');
|
|
38
38
|
return keys.reduce((node, key) => {
|
|
39
39
|
if (!node.children[key]) {
|
|
40
40
|
node.children[key] = { children: {} };
|
|
@@ -43,14 +43,12 @@ const ensureNode = (tree, path) => {
|
|
|
43
43
|
}, tree);
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
const main = (s, attrName =
|
|
46
|
+
const main = (s, attrName = 'vibe') => {
|
|
47
47
|
// Find element(s) with the specified attribute
|
|
48
48
|
const elements = document.querySelectorAll(`[${attrName}]`);
|
|
49
49
|
|
|
50
50
|
if (elements.length === 0) {
|
|
51
|
-
console.info(
|
|
52
|
-
`[vibe] No element found with attribute "${attrName}". Falling back to body.`,
|
|
53
|
-
);
|
|
51
|
+
console.info(`[vibe] No element found with attribute "${attrName}". Falling back to body.`);
|
|
54
52
|
} else if (elements.length > 1) {
|
|
55
53
|
console.info(
|
|
56
54
|
`[vibe] Multiple elements (${elements.length}) found with attribute "${attrName}". Hydrating the first one.`,
|
|
@@ -70,15 +68,23 @@ const main = (s, attrName = "vibe") => {
|
|
|
70
68
|
afterDomMutation: [],
|
|
71
69
|
};
|
|
72
70
|
|
|
73
|
-
// Observer reference -
|
|
71
|
+
// Observer reference and callback - defined here so state handler can access processMutations
|
|
74
72
|
let observer = null;
|
|
73
|
+
let processMutations = null;
|
|
75
74
|
|
|
76
75
|
const $ = state(s, (newState) => {
|
|
77
76
|
const mergedState = deepMerge($, newState);
|
|
78
77
|
const affectedElements = affected(parsedTree, previousState, mergedState);
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
// Capture pending mutations before disconnecting (takeRecords clears the queue)
|
|
80
|
+
let pendingMutations = [];
|
|
81
|
+
if (observer) {
|
|
82
|
+
pendingMutations = observer.takeRecords();
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
hydrate(affectedElements, mergedState, linkList);
|
|
87
|
+
|
|
82
88
|
if (observer) {
|
|
83
89
|
observer.observe(rootElement, {
|
|
84
90
|
attributes: false,
|
|
@@ -86,13 +92,16 @@ const main = (s, attrName = "vibe") => {
|
|
|
86
92
|
childList: true,
|
|
87
93
|
subtree: true,
|
|
88
94
|
});
|
|
95
|
+
|
|
96
|
+
// Process mutations that were pending before we disconnected
|
|
97
|
+
if (pendingMutations.length > 0 && processMutations) {
|
|
98
|
+
processMutations(pendingMutations);
|
|
99
|
+
}
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
const prev = structuredClone(previousState);
|
|
92
103
|
previousState = { ...$, ...newState };
|
|
93
|
-
hooks.afterUpdate.forEach((callback) =>
|
|
94
|
-
callback(structuredClone({ ...$ }), prev),
|
|
95
|
-
);
|
|
104
|
+
hooks.afterUpdate.forEach((callback) => callback(structuredClone({ ...$ }), prev));
|
|
96
105
|
});
|
|
97
106
|
|
|
98
107
|
// Add hook subscription method (non-enumerable so it won't be spread/cloned with state)
|
|
@@ -101,7 +110,7 @@ const main = (s, attrName = "vibe") => {
|
|
|
101
110
|
if (hooks[event]) {
|
|
102
111
|
hooks[event].push(callback);
|
|
103
112
|
}
|
|
104
|
-
return () => hooks[event] = hooks[event].filter((cb) => cb !== callback);
|
|
113
|
+
return () => (hooks[event] = hooks[event].filter((cb) => cb !== callback));
|
|
105
114
|
},
|
|
106
115
|
enumerable: false,
|
|
107
116
|
});
|
|
@@ -116,7 +125,8 @@ const main = (s, attrName = "vibe") => {
|
|
|
116
125
|
renderAllIterations(parsedTree, $, linkList);
|
|
117
126
|
renderAllConditionals(parsedTree, $, linkList);
|
|
118
127
|
|
|
119
|
-
observer
|
|
128
|
+
// Define observer callback as named function so we can call it manually for pending mutations
|
|
129
|
+
processMutations = (mutations) => {
|
|
120
130
|
// Early exit if no mutations to process (common case)
|
|
121
131
|
if (mutations.length === 0) return;
|
|
122
132
|
|
|
@@ -124,11 +134,8 @@ const main = (s, attrName = "vibe") => {
|
|
|
124
134
|
let parsedParents = null; // Lazy init - only create Set when needed
|
|
125
135
|
|
|
126
136
|
mutations.forEach(({ addedNodes, removedNodes, target }) => {
|
|
127
|
-
// Process removed nodes first (cleanup before additions)
|
|
128
137
|
removedNodes.forEach((node) => {
|
|
129
|
-
const entry = Object.entries(linkList).find(
|
|
130
|
-
([_, element]) => element === node,
|
|
131
|
-
);
|
|
138
|
+
const entry = Object.entries(linkList).find(([_, element]) => element === node);
|
|
132
139
|
|
|
133
140
|
// Skip nodes that aren't tracked (e.g., iteration-generated nodes or nodes outside reactive scope)
|
|
134
141
|
if (!entry) return;
|
|
@@ -136,9 +143,9 @@ const main = (s, attrName = "vibe") => {
|
|
|
136
143
|
const [dotAnnotation] = entry;
|
|
137
144
|
delete linkList[dotAnnotation];
|
|
138
145
|
|
|
139
|
-
const dotPath = dotAnnotation.split(
|
|
146
|
+
const dotPath = dotAnnotation.split('.');
|
|
140
147
|
const name = dotPath.pop();
|
|
141
|
-
const parentDotAnnotation = dotPath.join(
|
|
148
|
+
const parentDotAnnotation = dotPath.join('.');
|
|
142
149
|
|
|
143
150
|
const picked = navigateTree(parsedTree, parentDotAnnotation);
|
|
144
151
|
|
|
@@ -165,9 +172,7 @@ const main = (s, attrName = "vibe") => {
|
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
const entry = Object.entries(linkList).find(
|
|
169
|
-
([_, element]) => element === target,
|
|
170
|
-
);
|
|
175
|
+
const entry = Object.entries(linkList).find(([_, element]) => element === target);
|
|
171
176
|
|
|
172
177
|
// If parent isn't tracked, this node is outside the reactive scope
|
|
173
178
|
if (!entry) return;
|
|
@@ -206,6 +211,10 @@ const main = (s, attrName = "vibe") => {
|
|
|
206
211
|
|
|
207
212
|
hydrate(affectedElements, $, linkList);
|
|
208
213
|
|
|
214
|
+
// Process iterations and conditionals in the newly added node
|
|
215
|
+
renderAllIterations(parsedNode, $, linkList);
|
|
216
|
+
renderAllConditionals(parsedNode, $, linkList);
|
|
217
|
+
|
|
209
218
|
hadChanges = true;
|
|
210
219
|
});
|
|
211
220
|
});
|
|
@@ -214,7 +223,9 @@ const main = (s, attrName = "vibe") => {
|
|
|
214
223
|
if (hadChanges) {
|
|
215
224
|
hooks.afterDomMutation.forEach((callback) => callback());
|
|
216
225
|
}
|
|
217
|
-
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
observer = new MutationObserver(processMutations);
|
|
218
229
|
|
|
219
230
|
observer.observe(rootElement, {
|
|
220
231
|
attributes: false,
|
package/iterate.js
CHANGED
|
@@ -32,7 +32,7 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
32
32
|
parsed: originalTree.parsed,
|
|
33
33
|
element: clonedRoot,
|
|
34
34
|
children: {},
|
|
35
|
-
...(originalTree.attributes && { attributes: originalTree.attributes })
|
|
35
|
+
...(originalTree.attributes && { attributes: originalTree.attributes }),
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
if (!originalTree.children) return cloned;
|
|
@@ -50,7 +50,9 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
50
50
|
|
|
51
51
|
// Find corresponding comments in cloned DOM
|
|
52
52
|
const clonedStart = findComment(clonedChildNodes, startComment.textContent);
|
|
53
|
-
const clonedElse = elseComment
|
|
53
|
+
const clonedElse = elseComment
|
|
54
|
+
? findComment(clonedChildNodes, elseComment.textContent)
|
|
55
|
+
: null;
|
|
54
56
|
const clonedEnd = findComment(clonedChildNodes, endComment.textContent);
|
|
55
57
|
|
|
56
58
|
cloned.children[key] = {
|
|
@@ -60,14 +62,14 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
60
62
|
startComment: clonedStart || startComment,
|
|
61
63
|
elseComment: clonedElse,
|
|
62
64
|
endComment: clonedEnd || endComment,
|
|
63
|
-
branches // Branch templates are reused (they're cloned during mount)
|
|
65
|
+
branches, // Branch templates are reused (they're cloned during mount)
|
|
64
66
|
},
|
|
65
67
|
runtime: {
|
|
66
68
|
activeBranch: undefined,
|
|
67
69
|
activeInstance: null,
|
|
68
|
-
templateRemoved: false
|
|
70
|
+
templateRemoved: false,
|
|
69
71
|
},
|
|
70
|
-
children: {}
|
|
72
|
+
children: {},
|
|
71
73
|
};
|
|
72
74
|
continue;
|
|
73
75
|
}
|
|
@@ -88,13 +90,13 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
88
90
|
indexAlias: child.meta.indexAlias,
|
|
89
91
|
startComment: clonedStart || startComment,
|
|
90
92
|
endComment: clonedEnd || endComment,
|
|
91
|
-
template // Template is reused (cloned during render)
|
|
93
|
+
template, // Template is reused (cloned during render)
|
|
92
94
|
},
|
|
93
95
|
runtime: {
|
|
94
96
|
instances: [],
|
|
95
|
-
templateRemoved: false
|
|
97
|
+
templateRemoved: false,
|
|
96
98
|
},
|
|
97
|
-
children: {}
|
|
99
|
+
children: {},
|
|
98
100
|
};
|
|
99
101
|
continue;
|
|
100
102
|
}
|
|
@@ -150,7 +152,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
150
152
|
// Fallback for text-only templates
|
|
151
153
|
if (!firstElement && clonedNodes.length > 0) {
|
|
152
154
|
const container = document.createElement('span');
|
|
153
|
-
clonedNodes.forEach(node => container.appendChild(node.cloneNode(true)));
|
|
155
|
+
clonedNodes.forEach((node) => container.appendChild(node.cloneNode(true)));
|
|
154
156
|
firstElement = container;
|
|
155
157
|
}
|
|
156
158
|
|
|
@@ -165,7 +167,7 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
165
167
|
return {
|
|
166
168
|
element: firstElement,
|
|
167
169
|
tree,
|
|
168
|
-
clonedNodes
|
|
170
|
+
clonedNodes,
|
|
169
171
|
};
|
|
170
172
|
};
|
|
171
173
|
|
|
@@ -222,7 +224,7 @@ export const createScopedState = (globalState, localVars, parentScope = {}) => {
|
|
|
222
224
|
return { configurable: true, enumerable: true, value: parentScope[prop] };
|
|
223
225
|
}
|
|
224
226
|
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
225
|
-
}
|
|
227
|
+
},
|
|
226
228
|
});
|
|
227
229
|
};
|
|
228
230
|
|
|
@@ -247,11 +249,14 @@ export const renderAllIterations = (tree, state, linkList, parentScope = {}) =>
|
|
|
247
249
|
|
|
248
250
|
// Callback for rendering conditionals - set by conditionals.js to avoid circular import
|
|
249
251
|
let _renderAllConditionals = () => {};
|
|
250
|
-
export const setRenderAllConditionals = (fn) => {
|
|
252
|
+
export const setRenderAllConditionals = (fn) => {
|
|
253
|
+
_renderAllConditionals = fn;
|
|
254
|
+
};
|
|
251
255
|
|
|
252
256
|
// Initial render of an iteration block
|
|
253
257
|
export const renderIteration = (iterationNode, state, linkList, parentScope = {}) => {
|
|
254
|
-
const { arrayPath, itemAlias, indexAlias, template, startComment, endComment } =
|
|
258
|
+
const { arrayPath, itemAlias, indexAlias, template, startComment, endComment } =
|
|
259
|
+
iterationNode.meta;
|
|
255
260
|
|
|
256
261
|
// Already rendered - updates go through updateIteration
|
|
257
262
|
if (iterationNode.runtime.instances?.length > 0) return;
|
|
@@ -294,7 +299,7 @@ export const renderIteration = (iterationNode, state, linkList, parentScope = {}
|
|
|
294
299
|
const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
|
|
295
300
|
|
|
296
301
|
// Insert cloned nodes
|
|
297
|
-
clonedNodes.forEach(node => parent.insertBefore(node, endComment));
|
|
302
|
+
clonedNodes.forEach((node) => parent.insertBefore(node, endComment));
|
|
298
303
|
|
|
299
304
|
// Recursively render nested iterations and conditionals
|
|
300
305
|
if (tree) {
|
|
@@ -323,7 +328,11 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
|
|
|
323
328
|
const isFullToEmpty = oldArray.length > 0 && newArray.length === 0;
|
|
324
329
|
const isLargeArray = newArray.length > 100 || oldArray.length > 100;
|
|
325
330
|
|
|
326
|
-
if (
|
|
331
|
+
if (
|
|
332
|
+
window.__VIBE_FAST_ITERATION__ &&
|
|
333
|
+
fastPath.canUseFastPath(template) &&
|
|
334
|
+
(isEmptyToFull || isFullToEmpty || isLargeArray)
|
|
335
|
+
) {
|
|
327
336
|
fastPath.updateFast(iterationNode, newArray, newState, startComment, endComment);
|
|
328
337
|
return;
|
|
329
338
|
}
|
|
@@ -333,16 +342,26 @@ export const updateIteration = (iterationNode, newState, oldState, linkList, par
|
|
|
333
342
|
const newKeys = newArray.map((item, i) => getItemKey(item, i));
|
|
334
343
|
const operations = computeDiff(oldKeys, newKeys, oldArray, newArray);
|
|
335
344
|
|
|
336
|
-
operations.forEach(op => {
|
|
345
|
+
operations.forEach((op) => {
|
|
337
346
|
switch (op.type) {
|
|
338
|
-
case 'REMOVE':
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
case '
|
|
347
|
+
case 'REMOVE':
|
|
348
|
+
removeInstance(iterationNode, op.index);
|
|
349
|
+
break;
|
|
350
|
+
case 'ADD':
|
|
351
|
+
addInstance(iterationNode, op.item, op.index, newState, linkList, parentScope);
|
|
352
|
+
break;
|
|
353
|
+
case 'MOVE':
|
|
354
|
+
moveInstance(iterationNode, op.from, op.to);
|
|
355
|
+
break;
|
|
356
|
+
case 'UPDATE':
|
|
357
|
+
updateInstance(iterationNode, op.index, op.item, newState, linkList, parentScope);
|
|
358
|
+
break;
|
|
342
359
|
}
|
|
343
360
|
});
|
|
344
361
|
|
|
345
|
-
iterationNode.runtime.instances.forEach((inst, i) => {
|
|
362
|
+
iterationNode.runtime.instances.forEach((inst, i) => {
|
|
363
|
+
inst.index = i;
|
|
364
|
+
});
|
|
346
365
|
};
|
|
347
366
|
|
|
348
367
|
// Add a new instance at the specified index
|
|
@@ -356,12 +375,14 @@ const addInstance = (iterationNode, item, index, state, linkList, parentScope) =
|
|
|
356
375
|
const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
|
|
357
376
|
|
|
358
377
|
// Find insertion point
|
|
359
|
-
const insertBefore =
|
|
360
|
-
|
|
361
|
-
|
|
378
|
+
const insertBefore =
|
|
379
|
+
index < iterationNode.runtime.instances.length
|
|
380
|
+
? iterationNode.runtime.instances[index].clonedNodes?.[0] ||
|
|
381
|
+
iterationNode.runtime.instances[index].element
|
|
382
|
+
: endComment;
|
|
362
383
|
|
|
363
384
|
// Insert cloned nodes
|
|
364
|
-
clonedNodes.forEach(node => startComment.parentNode.insertBefore(node, insertBefore));
|
|
385
|
+
clonedNodes.forEach((node) => startComment.parentNode.insertBefore(node, insertBefore));
|
|
365
386
|
|
|
366
387
|
// Recursively render nested iterations and conditionals
|
|
367
388
|
if (tree) {
|
|
@@ -380,7 +401,9 @@ const removeInstance = (iterationNode, index) => {
|
|
|
380
401
|
const instance = iterationNode.runtime.instances[index];
|
|
381
402
|
|
|
382
403
|
// Remove all cloned nodes from DOM
|
|
383
|
-
(instance.clonedNodes || [instance.element]).forEach(node =>
|
|
404
|
+
(instance.clonedNodes || [instance.element]).forEach((node) =>
|
|
405
|
+
node?.parentNode?.removeChild(node),
|
|
406
|
+
);
|
|
384
407
|
|
|
385
408
|
iterationNode.runtime.instances.splice(index, 1);
|
|
386
409
|
};
|
|
@@ -401,11 +424,11 @@ const moveInstance = (iterationNode, fromIndex, toIndex) => {
|
|
|
401
424
|
// Find new insertion point
|
|
402
425
|
const nextInstance = iterationNode.runtime.instances[toIndex + 1];
|
|
403
426
|
const insertBefore = nextInstance
|
|
404
|
-
?
|
|
427
|
+
? nextInstance.clonedNodes?.[0] || nextInstance.element
|
|
405
428
|
: iterationNode.meta.endComment;
|
|
406
429
|
|
|
407
430
|
// Move all nodes
|
|
408
|
-
nodes.forEach(node => parent.insertBefore(node, insertBefore));
|
|
431
|
+
nodes.forEach((node) => parent.insertBefore(node, insertBefore));
|
|
409
432
|
};
|
|
410
433
|
|
|
411
434
|
// Update an instance with new item data
|
|
@@ -426,8 +449,8 @@ const updateInstance = (iterationNode, index, newItem, state, linkList, parentSc
|
|
|
426
449
|
const insertBefore = oldNodes[oldNodes.length - 1]?.nextSibling;
|
|
427
450
|
const parent = oldNodes[0]?.parentNode;
|
|
428
451
|
|
|
429
|
-
oldNodes.forEach(node => node?.parentNode?.removeChild(node));
|
|
430
|
-
clonedNodes.forEach(node => parent.insertBefore(node, insertBefore));
|
|
452
|
+
oldNodes.forEach((node) => node?.parentNode?.removeChild(node));
|
|
453
|
+
clonedNodes.forEach((node) => parent.insertBefore(node, insertBefore));
|
|
431
454
|
|
|
432
455
|
// Recursively render nested iterations and conditionals
|
|
433
456
|
if (tree) {
|
|
@@ -447,5 +470,5 @@ export default {
|
|
|
447
470
|
renderIteration,
|
|
448
471
|
updateIteration,
|
|
449
472
|
renderAllIterations,
|
|
450
|
-
createScopedState
|
|
473
|
+
createScopedState,
|
|
451
474
|
};
|