lume-js 0.4.1 → 1.0.0
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/README.md +42 -761
- package/package.json +13 -5
- package/src/addons/index.d.ts +173 -0
- package/src/addons/index.js +2 -1
- package/src/addons/repeat.js +342 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Minimal reactive state management using only standard JavaScript and HTML - no custom syntax, no build step required",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"dev": "vite",
|
|
21
|
+
"dev:site": "node scripts/build-site-assets.js && vite -c vite.site.config.js",
|
|
21
22
|
"build": "echo 'No build step needed - zero-runtime library!'",
|
|
22
23
|
"size": "node scripts/check-size.js",
|
|
23
24
|
"test": "vitest run",
|
|
@@ -57,12 +58,19 @@
|
|
|
57
58
|
"bugs": {
|
|
58
59
|
"url": "https://github.com/sathvikc/lume-js/issues"
|
|
59
60
|
},
|
|
60
|
-
"homepage": "https://github.
|
|
61
|
+
"homepage": "https://sathvikc.github.io/lume-js/",
|
|
61
62
|
"devDependencies": {
|
|
62
|
-
"
|
|
63
|
-
"
|
|
63
|
+
"@tailwindcss/postcss": "^4.1.17",
|
|
64
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
64
65
|
"@vitest/coverage-v8": "^2.1.4",
|
|
65
|
-
"
|
|
66
|
+
"autoprefixer": "^10.4.22",
|
|
67
|
+
"highlight.js": "^11.11.1",
|
|
68
|
+
"jsdom": "^25.0.1",
|
|
69
|
+
"marked": "^17.0.1",
|
|
70
|
+
"postcss": "^8.5.6",
|
|
71
|
+
"tailwindcss": "^4.1.17",
|
|
72
|
+
"vite": "^7.1.9",
|
|
73
|
+
"vitest": "^2.1.4"
|
|
66
74
|
},
|
|
67
75
|
"engines": {
|
|
68
76
|
"node": ">=20.19.0"
|
package/src/addons/index.d.ts
CHANGED
|
@@ -83,3 +83,176 @@ export function watch<T extends object, K extends keyof T>(
|
|
|
83
83
|
key: K,
|
|
84
84
|
callback: Subscriber<T[K]>
|
|
85
85
|
): Unsubscribe;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Context passed to preservation functions
|
|
89
|
+
*/
|
|
90
|
+
export interface PreservationContext {
|
|
91
|
+
/** Whether this update is a reorder (vs add/remove) */
|
|
92
|
+
isReorder?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Focus preservation function signature
|
|
97
|
+
*
|
|
98
|
+
* @param container - The list container element
|
|
99
|
+
* @returns Restore function to call after DOM updates, or null if nothing to restore
|
|
100
|
+
*/
|
|
101
|
+
export type FocusPreservation = (container: HTMLElement) => (() => void) | null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Scroll preservation function signature
|
|
105
|
+
*
|
|
106
|
+
* @param container - The list container element
|
|
107
|
+
* @param context - Additional context about the update
|
|
108
|
+
* @returns Restore function to call after DOM updates
|
|
109
|
+
*/
|
|
110
|
+
export type ScrollPreservation = (container: HTMLElement, context?: PreservationContext) => () => void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Options for the repeat() function
|
|
114
|
+
*/
|
|
115
|
+
export interface RepeatOptions<T> {
|
|
116
|
+
/** Function to extract unique key from item */
|
|
117
|
+
key: (item: T) => string | number;
|
|
118
|
+
|
|
119
|
+
/** Function to render/update an item's element */
|
|
120
|
+
render: (item: T, element: HTMLElement, index: number) => void;
|
|
121
|
+
|
|
122
|
+
/** Element tag name or factory function (default: 'div') */
|
|
123
|
+
element?: string | (() => HTMLElement);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Focus preservation strategy (default: defaultFocusPreservation)
|
|
127
|
+
* Set to null to disable focus preservation
|
|
128
|
+
*/
|
|
129
|
+
preserveFocus?: FocusPreservation | null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Scroll preservation strategy (default: defaultScrollPreservation)
|
|
133
|
+
* Set to null to disable scroll preservation
|
|
134
|
+
*/
|
|
135
|
+
preserveScroll?: ScrollPreservation | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Default focus preservation strategy
|
|
140
|
+
* Saves activeElement and selection state before DOM updates
|
|
141
|
+
*
|
|
142
|
+
* @param container - The list container element
|
|
143
|
+
* @returns Restore function or null
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* import { defaultFocusPreservation } from 'lume-js/addons';
|
|
148
|
+
*
|
|
149
|
+
* // Use in custom preservation wrapper
|
|
150
|
+
* const myPreservation = (container) => {
|
|
151
|
+
* console.log('Saving focus...');
|
|
152
|
+
* const restore = defaultFocusPreservation(container);
|
|
153
|
+
* return restore ? () => {
|
|
154
|
+
* restore();
|
|
155
|
+
* console.log('Focus restored!');
|
|
156
|
+
* } : null;
|
|
157
|
+
* };
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function defaultFocusPreservation(container: HTMLElement): (() => void) | null;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Default scroll preservation strategy
|
|
164
|
+
* Uses anchor-based preservation for add/remove, pixel position for reorder
|
|
165
|
+
*
|
|
166
|
+
* @param container - The list container element
|
|
167
|
+
* @param context - Additional context about the update
|
|
168
|
+
* @returns Restore function
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* import { defaultScrollPreservation } from 'lume-js/addons';
|
|
173
|
+
*
|
|
174
|
+
* // Wrap default behavior
|
|
175
|
+
* const myScrollPreservation = (container, context) => {
|
|
176
|
+
* const restore = defaultScrollPreservation(container, context);
|
|
177
|
+
* return () => {
|
|
178
|
+
* restore();
|
|
179
|
+
* console.log('Scroll position restored');
|
|
180
|
+
* };
|
|
181
|
+
* };
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export function defaultScrollPreservation(container: HTMLElement, context?: PreservationContext): () => void;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Efficiently render a list with element reuse by key.
|
|
188
|
+
*
|
|
189
|
+
* Features:
|
|
190
|
+
* - Element reuse (same DOM nodes, not recreated)
|
|
191
|
+
* - Minimal DOM operations (only updates what changed)
|
|
192
|
+
* - Optional focus preservation (maintains activeElement and selection)
|
|
193
|
+
* - Optional scroll preservation (intelligent positioning)
|
|
194
|
+
* - Fully customizable preservation strategies
|
|
195
|
+
*
|
|
196
|
+
* @param container - Container element or CSS selector
|
|
197
|
+
* @param store - Reactive state object
|
|
198
|
+
* @param arrayKey - Key in store containing the array
|
|
199
|
+
* @param options - Configuration options
|
|
200
|
+
* @returns Cleanup function
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* import { state } from 'lume-js';
|
|
205
|
+
* import { repeat } from 'lume-js/addons';
|
|
206
|
+
*
|
|
207
|
+
* const store = state({
|
|
208
|
+
* todos: [
|
|
209
|
+
* { id: 1, text: 'Learn Lume.js' },
|
|
210
|
+
* { id: 2, text: 'Build an app' }
|
|
211
|
+
* ]
|
|
212
|
+
* });
|
|
213
|
+
*
|
|
214
|
+
* // Basic usage (preservation enabled by default)
|
|
215
|
+
* const cleanup = repeat('#todo-list', store, 'todos', {
|
|
216
|
+
* key: todo => todo.id,
|
|
217
|
+
* render: (todo, el) => {
|
|
218
|
+
* if (!el.dataset.init) {
|
|
219
|
+
* el.innerHTML = `<input value="${todo.text}">`;
|
|
220
|
+
* el.dataset.init = 'true';
|
|
221
|
+
* }
|
|
222
|
+
* }
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* // Disable preservation (bare-bones)
|
|
226
|
+
* repeat('#list', store, 'todos', {
|
|
227
|
+
* key: todo => todo.id,
|
|
228
|
+
* render: (todo, el) => { el.textContent = todo.text; },
|
|
229
|
+
* preserveFocus: null,
|
|
230
|
+
* preserveScroll: null
|
|
231
|
+
* });
|
|
232
|
+
*
|
|
233
|
+
* // Custom preservation
|
|
234
|
+
* import { defaultFocusPreservation } from 'lume-js/addons';
|
|
235
|
+
*
|
|
236
|
+
* repeat('#list', store, 'todos', {
|
|
237
|
+
* key: todo => todo.id,
|
|
238
|
+
* render: (todo, el) => { ... },
|
|
239
|
+
* preserveFocus: (container) => {
|
|
240
|
+
* // Custom logic
|
|
241
|
+
* const restore = defaultFocusPreservation(container);
|
|
242
|
+
* return () => {
|
|
243
|
+
* restore?.();
|
|
244
|
+
* console.log('Focus restored');
|
|
245
|
+
* };
|
|
246
|
+
* }
|
|
247
|
+
* });
|
|
248
|
+
*
|
|
249
|
+
* // Cleanup
|
|
250
|
+
* cleanup();
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export function repeat<T>(
|
|
254
|
+
container: string | HTMLElement,
|
|
255
|
+
store: ReactiveState<any>,
|
|
256
|
+
arrayKey: string,
|
|
257
|
+
options: RepeatOptions<T>
|
|
258
|
+
): Unsubscribe;
|
package/src/addons/index.js
CHANGED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lume-JS List Rendering (Addon)
|
|
3
|
+
* @experimental
|
|
4
|
+
*
|
|
5
|
+
* Renders lists with automatic subscription and element reuse by key.
|
|
6
|
+
*
|
|
7
|
+
* Core guarantees:
|
|
8
|
+
* ✅ Element reuse by key (same DOM nodes, not recreated)
|
|
9
|
+
* ✅ Minimal DOM operations (only updates what changed)
|
|
10
|
+
* ✅ Memory efficiency (cleanup on remove)
|
|
11
|
+
*
|
|
12
|
+
* Default behavior (can be disabled/customized):
|
|
13
|
+
* ✅ Focus preservation (maintains activeElement and selection)
|
|
14
|
+
* ✅ Scroll preservation (intelligent positioning for add/remove/reorder)
|
|
15
|
+
*
|
|
16
|
+
* Philosophy: No artificial limitations
|
|
17
|
+
* - All preservation logic is overridable via options
|
|
18
|
+
* - Set to null/false to disable, or provide custom functions
|
|
19
|
+
* - Export utilities so you can wrap/extend them
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* import { repeat } from "lume-js/addons/repeat.js";
|
|
23
|
+
*
|
|
24
|
+
* // ⚠️ IMPORTANT: Arrays must be updated immutably!
|
|
25
|
+
* // store.items.push(x) // ❌ Won't trigger update
|
|
26
|
+
* // store.items = [...items] // ✅ Triggers update
|
|
27
|
+
*
|
|
28
|
+
* // Basic usage (focus & scroll preservation enabled by default)
|
|
29
|
+
* repeat('#list', store, 'todos', {
|
|
30
|
+
* key: todo => todo.id,
|
|
31
|
+
* render: (todo, el) => {
|
|
32
|
+
* if (!el.dataset.init) {
|
|
33
|
+
* el.innerHTML = `<input value="${todo.text}">`;
|
|
34
|
+
* el.dataset.init = 'true';
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Disable all preservation (bare-bones repeat)
|
|
40
|
+
* repeat('#list', store, 'items', {
|
|
41
|
+
* key: item => item.id,
|
|
42
|
+
* render: (item, el) => { el.textContent = item.name; },
|
|
43
|
+
* preserveFocus: null,
|
|
44
|
+
* preserveScroll: null
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // Custom focus preservation
|
|
48
|
+
* repeat('#list', store, 'items', {
|
|
49
|
+
* key: item => item.id,
|
|
50
|
+
* render: (item, el) => { ... },
|
|
51
|
+
* preserveFocus: (container) => {
|
|
52
|
+
* // Your custom logic
|
|
53
|
+
* const state = { focused: document.activeElement };
|
|
54
|
+
* return () => state.focused?.focus();
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Mix built-in and custom
|
|
59
|
+
* import { defaultFocusPreservation, defaultScrollPreservation } from "lume-js/addons/repeat.js";
|
|
60
|
+
*
|
|
61
|
+
* repeat('#list', store, 'items', {
|
|
62
|
+
* key: item => item.id,
|
|
63
|
+
* render: (item, el) => { ... },
|
|
64
|
+
* preserveFocus: defaultFocusPreservation, // use built-in
|
|
65
|
+
* preserveScroll: (container, context) => {
|
|
66
|
+
* // wrap/extend built-in
|
|
67
|
+
* const restore = defaultScrollPreservation(container, context);
|
|
68
|
+
* return () => {
|
|
69
|
+
* restore();
|
|
70
|
+
* console.log('Scroll restored!');
|
|
71
|
+
* };
|
|
72
|
+
* }
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// PRESERVATION UTILITIES (Exported for customization)
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default focus preservation strategy
|
|
82
|
+
* Saves activeElement and selection state before DOM updates
|
|
83
|
+
*
|
|
84
|
+
* @param {HTMLElement} container - The list container
|
|
85
|
+
* @returns {Function|null} Restore function, or null if nothing to restore
|
|
86
|
+
*/
|
|
87
|
+
export function defaultFocusPreservation(container) {
|
|
88
|
+
const activeEl = document.activeElement;
|
|
89
|
+
const shouldRestore = container.contains(activeEl);
|
|
90
|
+
|
|
91
|
+
if (!shouldRestore) return null;
|
|
92
|
+
|
|
93
|
+
let selectionStart = null;
|
|
94
|
+
let selectionEnd = null;
|
|
95
|
+
|
|
96
|
+
if (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA') {
|
|
97
|
+
selectionStart = activeEl.selectionStart;
|
|
98
|
+
selectionEnd = activeEl.selectionEnd;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
if (document.body.contains(activeEl)) {
|
|
103
|
+
activeEl.focus();
|
|
104
|
+
if (selectionStart !== null && selectionEnd !== null) {
|
|
105
|
+
activeEl.setSelectionRange(selectionStart, selectionEnd);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Default scroll preservation strategy
|
|
113
|
+
* Uses anchor-based preservation for add/remove, pixel position for reorder
|
|
114
|
+
*
|
|
115
|
+
* @param {HTMLElement} container - The list container
|
|
116
|
+
* @param {Object} context - Additional context
|
|
117
|
+
* @param {boolean} context.isReorder - Whether this is a reorder operation
|
|
118
|
+
* @returns {Function} Restore function
|
|
119
|
+
*/
|
|
120
|
+
export function defaultScrollPreservation(container, context = {}) {
|
|
121
|
+
const { isReorder = false } = context;
|
|
122
|
+
const scrollTop = container.scrollTop;
|
|
123
|
+
|
|
124
|
+
// Early return if no scroll
|
|
125
|
+
if (scrollTop === 0) {
|
|
126
|
+
return () => { container.scrollTop = 0; };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let anchorElement = null;
|
|
130
|
+
let anchorOffset = 0;
|
|
131
|
+
|
|
132
|
+
// Only use anchor-based preservation for add/remove, not reorder
|
|
133
|
+
if (!isReorder) {
|
|
134
|
+
const containerRect = container.getBoundingClientRect();
|
|
135
|
+
// Avoid Array.from - iterate children directly
|
|
136
|
+
for (let child = container.firstElementChild; child; child = child.nextElementSibling) {
|
|
137
|
+
const rect = child.getBoundingClientRect();
|
|
138
|
+
|
|
139
|
+
if (rect.bottom > containerRect.top) {
|
|
140
|
+
anchorElement = child;
|
|
141
|
+
anchorOffset = rect.top - containerRect.top;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
if (anchorElement && document.body.contains(anchorElement)) {
|
|
149
|
+
const newRect = anchorElement.getBoundingClientRect();
|
|
150
|
+
const containerRect = container.getBoundingClientRect();
|
|
151
|
+
const currentOffset = newRect.top - containerRect.top;
|
|
152
|
+
const scrollAdjustment = currentOffset - anchorOffset;
|
|
153
|
+
|
|
154
|
+
container.scrollTop = container.scrollTop + scrollAdjustment;
|
|
155
|
+
} else {
|
|
156
|
+
container.scrollTop = scrollTop;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// MAIN REPEAT FUNCTION
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Efficiently render a list with element reuse
|
|
167
|
+
*
|
|
168
|
+
* @param {string|HTMLElement} container - Container element or selector
|
|
169
|
+
* @param {Object} store - Reactive state object
|
|
170
|
+
* @param {string} arrayKey - Key in store containing the array
|
|
171
|
+
* @param {Object} options - Configuration
|
|
172
|
+
* @param {Function} options.key - Function to extract unique key: (item) => key
|
|
173
|
+
* @param {Function} options.render - Function to render item: (item, element, index) => void
|
|
174
|
+
* @param {string|Function} [options.element='div'] - Element tag name or factory function
|
|
175
|
+
* @param {Function|null} [options.preserveFocus=defaultFocusPreservation] - Focus preservation strategy (null to disable)
|
|
176
|
+
* @param {Function|null} [options.preserveScroll=defaultScrollPreservation] - Scroll preservation strategy (null to disable)
|
|
177
|
+
* @returns {Function} Cleanup function
|
|
178
|
+
*/
|
|
179
|
+
export function repeat(container, store, arrayKey, options) {
|
|
180
|
+
const {
|
|
181
|
+
key,
|
|
182
|
+
render,
|
|
183
|
+
element = 'div',
|
|
184
|
+
preserveFocus = defaultFocusPreservation,
|
|
185
|
+
preserveScroll = defaultScrollPreservation
|
|
186
|
+
} = options;
|
|
187
|
+
|
|
188
|
+
// Resolve container
|
|
189
|
+
const containerEl =
|
|
190
|
+
typeof container === 'string'
|
|
191
|
+
? document.querySelector(container)
|
|
192
|
+
: container;
|
|
193
|
+
|
|
194
|
+
if (!containerEl) {
|
|
195
|
+
console.warn(`[Lume.js] repeat(): container "${container}" not found`);
|
|
196
|
+
return () => { };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (typeof key !== 'function') {
|
|
200
|
+
throw new Error('[Lume.js] repeat(): options.key must be a function');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (typeof render !== 'function') {
|
|
204
|
+
throw new Error('[Lume.js] repeat(): options.render must be a function');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// key -> HTMLElement
|
|
208
|
+
const elementsByKey = new Map();
|
|
209
|
+
const seenKeys = new Set();
|
|
210
|
+
|
|
211
|
+
function createElement() {
|
|
212
|
+
return typeof element === 'function'
|
|
213
|
+
? element()
|
|
214
|
+
: document.createElement(element);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function updateList() {
|
|
218
|
+
const items = store[arrayKey];
|
|
219
|
+
|
|
220
|
+
if (!Array.isArray(items)) {
|
|
221
|
+
console.warn(`[Lume.js] repeat(): store.${arrayKey} is not an array`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Skip preservation if container is not in document (performance optimization)
|
|
226
|
+
const shouldPreserve = document.body.contains(containerEl);
|
|
227
|
+
|
|
228
|
+
// Only compute isReorder if scroll preservation needs it
|
|
229
|
+
let isReorder = false;
|
|
230
|
+
if (shouldPreserve && preserveScroll) {
|
|
231
|
+
const previousKeys = new Set(elementsByKey.keys());
|
|
232
|
+
const currentKeys = new Set(items.map(item => key(item)));
|
|
233
|
+
isReorder = previousKeys.size === currentKeys.size &&
|
|
234
|
+
[...previousKeys].every(k => currentKeys.has(k));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Save state before DOM manipulation
|
|
238
|
+
const restoreFocus = shouldPreserve && preserveFocus ? preserveFocus(containerEl) : null;
|
|
239
|
+
const restoreScroll = shouldPreserve && preserveScroll ? preserveScroll(containerEl, { isReorder }) : null;
|
|
240
|
+
|
|
241
|
+
seenKeys.clear();
|
|
242
|
+
const nextKeys = new Set();
|
|
243
|
+
const nextEls = [];
|
|
244
|
+
|
|
245
|
+
// Build ordered list of DOM nodes (created or reused)
|
|
246
|
+
for (let i = 0; i < items.length; i++) {
|
|
247
|
+
const item = items[i];
|
|
248
|
+
const k = key(item);
|
|
249
|
+
|
|
250
|
+
if (seenKeys.has(k)) {
|
|
251
|
+
console.warn(`[Lume.js] repeat(): duplicate key "${k}"`);
|
|
252
|
+
}
|
|
253
|
+
seenKeys.add(k);
|
|
254
|
+
nextKeys.add(k);
|
|
255
|
+
|
|
256
|
+
let el = elementsByKey.get(k);
|
|
257
|
+
const isNew = !el;
|
|
258
|
+
|
|
259
|
+
if (isNew) {
|
|
260
|
+
el = createElement();
|
|
261
|
+
elementsByKey.set(k, el);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
if (isNew) {
|
|
266
|
+
el.__lume_new = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
render(item, el, i);
|
|
270
|
+
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`[Lume.js] repeat(): error rendering key "${k}"`, err);
|
|
273
|
+
} finally {
|
|
274
|
+
delete el.__lume_new;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
nextEls.push(el);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Reconcile actual DOM ordering
|
|
281
|
+
let ptr = containerEl.firstChild;
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < nextEls.length; i++) {
|
|
284
|
+
const desired = nextEls[i];
|
|
285
|
+
|
|
286
|
+
if (ptr === desired) {
|
|
287
|
+
ptr = ptr.nextSibling;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
containerEl.insertBefore(desired, ptr);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Remove leftover children not in nextEls
|
|
295
|
+
while (ptr) {
|
|
296
|
+
const next = ptr.nextSibling;
|
|
297
|
+
containerEl.removeChild(ptr);
|
|
298
|
+
ptr = next;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Clean map: remove keys not in nextKeys
|
|
302
|
+
// Iterate over elementsByKey entries and delete if not in nextKeys
|
|
303
|
+
if (elementsByKey.size !== nextKeys.size) {
|
|
304
|
+
for (const k of elementsByKey.keys()) {
|
|
305
|
+
if (!nextKeys.has(k)) {
|
|
306
|
+
elementsByKey.delete(k);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Restore state after DOM manipulation
|
|
312
|
+
if (restoreFocus) restoreFocus();
|
|
313
|
+
if (restoreScroll) restoreScroll();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Initial render
|
|
317
|
+
updateList();
|
|
318
|
+
|
|
319
|
+
// Subscription
|
|
320
|
+
let unsubscribe;
|
|
321
|
+
if (typeof store.$subscribe === 'function') {
|
|
322
|
+
unsubscribe = store.$subscribe(arrayKey, updateList);
|
|
323
|
+
} else if (typeof store.subscribe === 'function') {
|
|
324
|
+
// Generic subscribe (e.g. computed)
|
|
325
|
+
unsubscribe = store.subscribe(() => updateList());
|
|
326
|
+
} else {
|
|
327
|
+
console.warn('[Lume.js] repeat(): store is not reactive (no $subscribe or subscribe method)');
|
|
328
|
+
return () => {
|
|
329
|
+
containerEl.replaceChildren();
|
|
330
|
+
elementsByKey.clear();
|
|
331
|
+
seenKeys.clear();
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return () => {
|
|
336
|
+
unsubscribe();
|
|
337
|
+
// Clear DOM elements (replaceChildren is faster than loop)
|
|
338
|
+
containerEl.replaceChildren();
|
|
339
|
+
elementsByKey.clear();
|
|
340
|
+
seenKeys.clear();
|
|
341
|
+
};
|
|
342
|
+
}
|