apprun 3.36.0 → 3.36.1
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 +28 -0
- package/README.md +133 -20
- package/WHATSNEW.md +20 -12
- package/apprun-book.jpg +0 -0
- package/apprun.d.ts +5 -2
- package/cli/app.js +29 -0
- package/cli/index.html +13 -0
- package/{apprun-cli.js → cli/index.js} +8 -14
- package/dist/apprun-dev-tools.js +1 -2
- package/dist/apprun-dev-tools.js.map +1 -1
- package/dist/apprun-html.esm.js +7 -7
- package/dist/apprun-html.esm.js.map +1 -1
- package/dist/apprun-html.js +1 -1
- package/dist/apprun-html.js.map +1 -1
- package/dist/apprun-play-html.esm.js +7 -7
- package/dist/apprun-play-html.esm.js.map +1 -1
- package/dist/apprun-play.js +1 -1
- package/dist/apprun-play.js.map +1 -1
- package/dist/apprun.esm.js +1 -1
- package/dist/apprun.esm.js.map +1 -1
- package/dist/apprun.js +1 -1
- package/dist/apprun.js.map +1 -1
- package/esm/app.js +10 -6
- package/esm/app.js.map +1 -1
- package/esm/apprun-dev-tools.js +1 -7
- package/esm/apprun-dev-tools.js.map +1 -1
- package/esm/apprun.js +18 -4
- package/esm/apprun.js.map +1 -1
- package/esm/component.js +4 -0
- package/esm/component.js.map +1 -1
- package/esm/router.js +241 -18
- package/esm/router.js.map +1 -1
- package/esm/vdom-my-new.js +329 -0
- package/esm/vdom-my-new.js.map +1 -0
- package/esm/vdom-my-prop-attr.js +227 -0
- package/esm/vdom-my-prop-attr.js.map +1 -0
- package/esm/vdom-my.js +109 -143
- package/esm/vdom-my.js.map +1 -1
- package/esm/version.js +1 -1
- package/index.html +1 -1
- package/jest.config.js +2 -1
- package/jest.setup.js +29 -3
- package/jsx-runtime.js +1 -1
- package/jsx-runtime.js.map +1 -1
- package/package.json +7 -7
- package/src/app.ts +11 -6
- package/src/apprun-dev-tools.tsx +1 -7
- package/src/apprun.ts +22 -4
- package/src/component.ts +4 -0
- package/src/router.ts +259 -15
- package/src/vdom-my-new.ts +311 -0
- package/src/vdom-my-prop-attr.ts +241 -0
- package/src/vdom-my.ts +109 -134
- package/src/version.ts +1 -1
- package/cli/export.js +0 -92
- package/cli/import.js +0 -68
- package/plan/plan-apprun-bugfixes.md +0 -207
package/src/router.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AppRun Router Implementation
|
|
2
|
+
* AppRun Router Implementation with Hierarchical Matching
|
|
3
3
|
*
|
|
4
4
|
* This file provides URL routing capabilities:
|
|
5
5
|
* 1. Hash-based routing (#/path)
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - Route parameter parsing and injection into events
|
|
23
23
|
* - URL pattern matching with parameter support
|
|
24
24
|
* - Global and component-level route handling
|
|
25
|
+
* - **NEW: Hierarchical route matching** - progressively tries parent routes
|
|
25
26
|
*
|
|
26
27
|
* Type Safety Improvements (v3.35.1):
|
|
27
28
|
* - Enhanced route type definitions
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
*
|
|
31
32
|
* Usage:
|
|
32
33
|
* ```ts
|
|
33
|
-
* //
|
|
34
|
+
* // Basic routing
|
|
34
35
|
* app.on('#/home', () => // Show home page);
|
|
35
36
|
* app.on('#/users/:id', (id) => // Show user profile);
|
|
36
37
|
* app.on('/api/*', (path) => // Handle API routes);
|
|
@@ -38,11 +39,264 @@
|
|
|
38
39
|
* // Navigate programmatically
|
|
39
40
|
* app.run('route', '#/home');
|
|
40
41
|
* route('/users/123'); // Direct routing
|
|
42
|
+
*
|
|
43
|
+
* // Hierarchical matching examples
|
|
44
|
+
* app.on('/api', (operation, id) => // Handle /api/users/123);
|
|
45
|
+
* app.on('#users', (id, action) => // Handle #users/123/edit);
|
|
46
|
+
*
|
|
47
|
+
* // Hierarchical Route Matching (NEW):
|
|
48
|
+
* // For URL: /api/v1/users/123
|
|
49
|
+
* // Router tries: /api/v1/users/123 → /api/v1/users → /api/v1 → /api → 404
|
|
50
|
+
* // If handler found at /api, it receives: ('v1', 'users', '123')
|
|
51
|
+
*
|
|
52
|
+
* // Base Path Support (NEW):
|
|
53
|
+
* app.basePath = '/myapp'; // For sub-directory deployments
|
|
54
|
+
* // Links: <a href="/users/123"> (relative paths)
|
|
55
|
+
* // Navigation: /myapp/users/123 (full path)
|
|
56
|
+
* // Routing: /users/123 (base path stripped)
|
|
57
|
+
*
|
|
58
|
+
* // Empty Path Handling (NEW):
|
|
59
|
+
* // For URL: "" (empty)
|
|
60
|
+
* // Router tries: # → / → #/ → 404 (in priority order)
|
|
61
|
+
*
|
|
62
|
+
* // 404 Behavior (ENHANCED):
|
|
63
|
+
* // Fires only at minimal levels: /a, #a, #/a, a
|
|
64
|
+
* // Never tries root handlers: /, #, #/
|
|
41
65
|
* ```
|
|
42
66
|
*/
|
|
43
67
|
|
|
44
68
|
import app from './app';
|
|
45
69
|
|
|
70
|
+
// Helper functions for hierarchical routing
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract clean path segments from URL
|
|
74
|
+
* @param url - The URL to parse
|
|
75
|
+
* @returns Array of path segments
|
|
76
|
+
*/
|
|
77
|
+
function parsePathSegments(url: string): string[] {
|
|
78
|
+
if (!url) return [];
|
|
79
|
+
|
|
80
|
+
// Handle different URL types
|
|
81
|
+
if (url.startsWith('#/')) {
|
|
82
|
+
return url.substring(2).split('/');
|
|
83
|
+
} else if (url.startsWith('#')) {
|
|
84
|
+
return url.substring(1).split('/');
|
|
85
|
+
} else if (url.startsWith('/')) {
|
|
86
|
+
return url.substring(1).split('/');
|
|
87
|
+
} else {
|
|
88
|
+
return url.split('/');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalize trailing slash - convert /a/ to /a
|
|
94
|
+
* @param url - The URL to normalize
|
|
95
|
+
* @returns Normalized URL
|
|
96
|
+
*/
|
|
97
|
+
function normalizeTrailingSlash(url: string): string {
|
|
98
|
+
if (!url || url === '/' || url === '#' || url === '#/') return url;
|
|
99
|
+
if (url.endsWith('/')) return url.slice(0, -1);
|
|
100
|
+
return url;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate hierarchy depth and warn if too deep
|
|
105
|
+
* @param segments - Path segments to validate
|
|
106
|
+
*/
|
|
107
|
+
function validateHierarchyDepth(segments: string[]): void {
|
|
108
|
+
// Count non-empty segments for depth validation
|
|
109
|
+
const nonEmptySegments = segments.filter(Boolean);
|
|
110
|
+
if (nonEmptySegments.length > 11) {
|
|
111
|
+
console.warn(`Deep route hierarchy detected: ${nonEmptySegments.join('/')} (${nonEmptySegments.length} levels)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Strip base path from URL
|
|
117
|
+
* @param url - The URL to process
|
|
118
|
+
* @param basePath - The base path to remove
|
|
119
|
+
* @returns URL with base path removed
|
|
120
|
+
*/
|
|
121
|
+
function stripBasePath(url: string, basePath: string): string {
|
|
122
|
+
if (!basePath || basePath === '/' || basePath === '') return url;
|
|
123
|
+
|
|
124
|
+
// Normalize base path
|
|
125
|
+
const normalizedBasePath = basePath.startsWith('/') ? basePath : '/' + basePath;
|
|
126
|
+
|
|
127
|
+
if (url.startsWith(normalizedBasePath)) {
|
|
128
|
+
const stripped = url.substring(normalizedBasePath.length);
|
|
129
|
+
return stripped.startsWith('/') ? stripped : '/' + stripped;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return url;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate hierarchy of routes to try (stops at minimal level)
|
|
137
|
+
* @param segments - Array of path segments
|
|
138
|
+
* @param routeType - Type of route (path, hash, hash-slash, non-prefixed)
|
|
139
|
+
* @returns Array of route names to try in order
|
|
140
|
+
*/
|
|
141
|
+
function generateRouteHierarchy(segments: string[], routeType: 'path' | 'hash' | 'hash-slash' | 'non-prefixed'): string[] {
|
|
142
|
+
const hierarchy: string[] = [];
|
|
143
|
+
|
|
144
|
+
// Build hierarchy from most specific to least specific
|
|
145
|
+
for (let i = segments.length; i > 0; i--) {
|
|
146
|
+
const currentSegments = segments.slice(0, i);
|
|
147
|
+
let routeName = '';
|
|
148
|
+
|
|
149
|
+
switch (routeType) {
|
|
150
|
+
case 'path':
|
|
151
|
+
routeName = '/' + currentSegments.join('/');
|
|
152
|
+
break;
|
|
153
|
+
case 'hash':
|
|
154
|
+
routeName = '#' + currentSegments.join('/');
|
|
155
|
+
break;
|
|
156
|
+
case 'hash-slash':
|
|
157
|
+
routeName = '#/' + currentSegments.join('/');
|
|
158
|
+
break;
|
|
159
|
+
case 'non-prefixed':
|
|
160
|
+
routeName = currentSegments.join('/');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
hierarchy.push(routeName);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return hierarchy;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Find handler in hierarchy and return handler info
|
|
172
|
+
* @param hierarchy - Array of route names to try
|
|
173
|
+
* @param originalSegments - Original path segments
|
|
174
|
+
* @returns Handler info if found, null otherwise
|
|
175
|
+
*/
|
|
176
|
+
function findHandlerInHierarchy(hierarchy: string[], originalSegments: string[]): { eventName: string; parameters: string[] } | null {
|
|
177
|
+
for (let i = 0; i < hierarchy.length; i++) {
|
|
178
|
+
const routeName = hierarchy[i];
|
|
179
|
+
const subscribers = app.find(routeName);
|
|
180
|
+
|
|
181
|
+
if (subscribers && subscribers.length > 0) {
|
|
182
|
+
// Found handler - calculate remaining parameters
|
|
183
|
+
const handlerDepth = hierarchy.length - i;
|
|
184
|
+
const parameters = originalSegments.slice(handlerDepth);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
eventName: routeName,
|
|
188
|
+
parameters: parameters
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Handle empty path with priority order: # → / → #/ → 404
|
|
198
|
+
*/
|
|
199
|
+
function handleEmptyPath(): void {
|
|
200
|
+
// Try # first
|
|
201
|
+
const hashSubscribers = app.find('#');
|
|
202
|
+
if (hashSubscribers && hashSubscribers.length > 0) {
|
|
203
|
+
app.run('#');
|
|
204
|
+
app.run(ROUTER_EVENT, '#');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try / second
|
|
209
|
+
const pathSubscribers = app.find('/');
|
|
210
|
+
if (pathSubscribers && pathSubscribers.length > 0) {
|
|
211
|
+
app.run('/');
|
|
212
|
+
app.run(ROUTER_EVENT, '/');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Try #/ third
|
|
217
|
+
const hashSlashSubscribers = app.find('#/');
|
|
218
|
+
if (hashSlashSubscribers && hashSlashSubscribers.length > 0) {
|
|
219
|
+
app.run('#/');
|
|
220
|
+
app.run(ROUTER_EVENT, '#/');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fire 404 if no handlers found
|
|
225
|
+
console.warn('No subscribers for event: ');
|
|
226
|
+
app.run(ROUTER_404_EVENT, '');
|
|
227
|
+
app.run(ROUTER_EVENT, '');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Main hierarchical routing logic
|
|
232
|
+
* @param url - The URL to route
|
|
233
|
+
*/
|
|
234
|
+
function routeWithHierarchy(url: string): void {
|
|
235
|
+
// Handle empty path
|
|
236
|
+
if (!url) {
|
|
237
|
+
handleEmptyPath();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Normalize trailing slash
|
|
242
|
+
url = normalizeTrailingSlash(url);
|
|
243
|
+
|
|
244
|
+
// Strip base path if configured
|
|
245
|
+
const basePath = app['basePath'];
|
|
246
|
+
if (basePath) {
|
|
247
|
+
url = stripBasePath(url, basePath);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Parse segments and validate depth
|
|
251
|
+
const segments = parsePathSegments(url);
|
|
252
|
+
validateHierarchyDepth(segments);
|
|
253
|
+
|
|
254
|
+
// Determine route type
|
|
255
|
+
let routeType: 'path' | 'hash' | 'hash-slash' | 'non-prefixed';
|
|
256
|
+
if (url.startsWith('#/')) {
|
|
257
|
+
routeType = 'hash-slash';
|
|
258
|
+
} else if (url.startsWith('#')) {
|
|
259
|
+
routeType = 'hash';
|
|
260
|
+
} else if (url.startsWith('/')) {
|
|
261
|
+
routeType = 'path';
|
|
262
|
+
} else {
|
|
263
|
+
routeType = 'non-prefixed';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Generate hierarchy
|
|
267
|
+
const hierarchy = generateRouteHierarchy(segments, routeType);
|
|
268
|
+
|
|
269
|
+
// Find handler in hierarchy
|
|
270
|
+
const handlerInfo = findHandlerInHierarchy(hierarchy, segments);
|
|
271
|
+
|
|
272
|
+
if (handlerInfo) {
|
|
273
|
+
// Found handler - publish route with parameters
|
|
274
|
+
publishRoute(handlerInfo.eventName, ...handlerInfo.parameters);
|
|
275
|
+
} else {
|
|
276
|
+
// No handler found - fire 404 with original URL
|
|
277
|
+
if (hierarchy.length > 0) {
|
|
278
|
+
const minimalRoute = hierarchy[hierarchy.length - 1];
|
|
279
|
+
console.warn(`No subscribers for event: ${minimalRoute}`);
|
|
280
|
+
app.run(ROUTER_404_EVENT, url);
|
|
281
|
+
app.run(ROUTER_EVENT, url);
|
|
282
|
+
} else {
|
|
283
|
+
handleEmptyPath();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const publishRoute = (name: string, ...args: any[]) => {
|
|
289
|
+
if (!name || name === ROUTER_EVENT || name === ROUTER_404_EVENT) return;
|
|
290
|
+
const subscribers = app.find(name);
|
|
291
|
+
if (!subscribers || subscribers.length === 0) {
|
|
292
|
+
console.warn(`No subscribers for event: ${name}`);
|
|
293
|
+
app.run(ROUTER_404_EVENT, name, ...args);
|
|
294
|
+
} else {
|
|
295
|
+
app.run(name, ...args);
|
|
296
|
+
}
|
|
297
|
+
app.run(ROUTER_EVENT, name, ...args);
|
|
298
|
+
}
|
|
299
|
+
|
|
46
300
|
export type Route = (url: string, ...args: any[]) => any;
|
|
47
301
|
|
|
48
302
|
export const ROUTER_EVENT: string = '//';
|
|
@@ -50,18 +304,8 @@ export const ROUTER_404_EVENT: string = '///';
|
|
|
50
304
|
export const route: Route = (url: string) => {
|
|
51
305
|
if (app['lastUrl'] === url) return; // Prevent duplicate routing
|
|
52
306
|
app['lastUrl'] = url;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
app.run(name, ...rest) || app.run(ROUTER_404_EVENT, name, ...rest);
|
|
57
|
-
app.run(ROUTER_EVENT, name, ...rest);
|
|
58
|
-
} else if (url.startsWith('/')) {
|
|
59
|
-
const [_, name, ...rest] = url.split('/');
|
|
60
|
-
app.run('/' + name, ...rest) || app.run(ROUTER_404_EVENT, '/' + name, ...rest);
|
|
61
|
-
app.run(ROUTER_EVENT, '/' + name, ...rest);
|
|
62
|
-
} else {
|
|
63
|
-
app.run(url) || app.run(ROUTER_404_EVENT, url);
|
|
64
|
-
app.run(ROUTER_EVENT, url);
|
|
65
|
-
}
|
|
307
|
+
|
|
308
|
+
// Use hierarchical routing logic
|
|
309
|
+
routeWithHierarchy(url);
|
|
66
310
|
}
|
|
67
311
|
export default route;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { VDOM, VNode } from './types';
|
|
2
|
+
import directive from './directive';
|
|
3
|
+
export type Element = any; //HTMLElement | SVGSVGElement | SVGElement;
|
|
4
|
+
|
|
5
|
+
export function Fragment(props, ...children): any[] {
|
|
6
|
+
return collect(children);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ATTR_PROPS = '_props';
|
|
10
|
+
|
|
11
|
+
function collect(children) {
|
|
12
|
+
const ch = [];
|
|
13
|
+
const push = (c) => {
|
|
14
|
+
if (c !== null && c !== undefined && c !== '' && c !== false) {
|
|
15
|
+
ch.push((typeof c === 'function' || typeof c === 'object') ? c : `${c}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
children && children.forEach(c => {
|
|
19
|
+
if (Array.isArray(c)) {
|
|
20
|
+
c.forEach(i => push(i));
|
|
21
|
+
} else {
|
|
22
|
+
push(c);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return ch;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createElement(tag: string | Function | [], props?: {}, ...children) {
|
|
29
|
+
const ch = collect(children);
|
|
30
|
+
if (typeof tag === 'string') return { tag, props, children: ch };
|
|
31
|
+
else if (Array.isArray(tag)) return tag; // JSX fragments - babel
|
|
32
|
+
else if (tag === undefined && children) return ch; // JSX fragments - typescript
|
|
33
|
+
else if (Object.getPrototypeOf(tag).__isAppRunComponent) return { tag, props, children: ch } // createComponent(tag, { ...props, children });
|
|
34
|
+
else if (typeof tag === 'function') return tag(props, ch);
|
|
35
|
+
else throw new Error(`Unknown tag in vdom ${tag}`);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const keyCache = {};
|
|
39
|
+
|
|
40
|
+
export const updateElement = (element: Element | string, nodes: VDOM, component = {}) => {
|
|
41
|
+
// tslint:disable-next-line
|
|
42
|
+
if (nodes == null || nodes === false) return;
|
|
43
|
+
const el = (typeof element === 'string' && element) ?
|
|
44
|
+
document.getElementById(element) || document.querySelector(element) : element;
|
|
45
|
+
nodes = directive(nodes, component);
|
|
46
|
+
render(el, nodes, component);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function render(element: Element, nodes: VDOM, parent = {}) {
|
|
50
|
+
// tslint:disable-next-line
|
|
51
|
+
if (nodes == null || nodes === false) return;
|
|
52
|
+
nodes = createComponent(nodes, parent);
|
|
53
|
+
if (!element) return;
|
|
54
|
+
const isSvg = element.nodeName === "SVG";
|
|
55
|
+
if (Array.isArray(nodes)) {
|
|
56
|
+
updateChildren(element, nodes, isSvg);
|
|
57
|
+
} else {
|
|
58
|
+
updateChildren(element, [nodes], isSvg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function same(el: Element, node: VNode) {
|
|
63
|
+
// if (!el || !node) return false;
|
|
64
|
+
const key1 = el.nodeName;
|
|
65
|
+
const key2 = `${node.tag || ''}`;
|
|
66
|
+
return key1.toUpperCase() === key2.toUpperCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function update(element: Element, node: VNode, isSvg: boolean) {
|
|
70
|
+
// console.assert(!!element);
|
|
71
|
+
isSvg = isSvg || node.tag === "svg";
|
|
72
|
+
if (!same(element, node)) {
|
|
73
|
+
element.parentNode.replaceChild(create(node, isSvg), element);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
updateChildren(element, node.children, isSvg);
|
|
77
|
+
updateProps(element, node.props, isSvg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function updateChildren(element: Element, children: any[], isSvg: boolean) {
|
|
81
|
+
const old_len = element.childNodes?.length || 0;
|
|
82
|
+
const new_len = children?.length || 0;
|
|
83
|
+
|
|
84
|
+
// Handle key-based reordering first if any children have keys
|
|
85
|
+
const hasKeysInNewChildren = children?.some(child =>
|
|
86
|
+
child && typeof child === 'object' && child.props && child.props.key !== undefined
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (hasKeysInNewChildren) {
|
|
90
|
+
// Create a map of existing keyed elements
|
|
91
|
+
const existingKeyedElements = new Map();
|
|
92
|
+
for (let i = 0; i < old_len; i++) {
|
|
93
|
+
const el = element.childNodes[i];
|
|
94
|
+
if (el && el.key) {
|
|
95
|
+
existingKeyedElements.set(el.key, el);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Build new DOM structure
|
|
100
|
+
const fragment = document.createDocumentFragment();
|
|
101
|
+
for (let i = 0; i < new_len; i++) {
|
|
102
|
+
const child = children[i];
|
|
103
|
+
if (child == null) continue;
|
|
104
|
+
|
|
105
|
+
const key = child.props && child.props['key'];
|
|
106
|
+
if (key && existingKeyedElements.has(key)) {
|
|
107
|
+
// Reuse existing element
|
|
108
|
+
const existingEl = existingKeyedElements.get(key);
|
|
109
|
+
update(existingEl, child as VNode, isSvg);
|
|
110
|
+
fragment.appendChild(existingEl);
|
|
111
|
+
existingKeyedElements.delete(key); // Mark as used
|
|
112
|
+
} else {
|
|
113
|
+
// Create new element
|
|
114
|
+
fragment.appendChild(create(child, isSvg));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Clear current children and append new structure
|
|
119
|
+
while (element.firstChild) {
|
|
120
|
+
element.removeChild(element.firstChild);
|
|
121
|
+
}
|
|
122
|
+
element.appendChild(fragment);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Original non-keyed logic
|
|
127
|
+
const len = Math.min(old_len, new_len);
|
|
128
|
+
for (let i = 0; i < len; i++) {
|
|
129
|
+
const child = children[i];
|
|
130
|
+
if (child == null) continue;
|
|
131
|
+
const el = element.childNodes[i];
|
|
132
|
+
if (!el) continue; // Safety check for undefined childNodes
|
|
133
|
+
if (typeof child === 'string') {
|
|
134
|
+
if (el.nodeType === 3) {
|
|
135
|
+
if (el.nodeValue !== child) {
|
|
136
|
+
el.nodeValue = child;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
element.replaceChild(createText(child), el);
|
|
140
|
+
}
|
|
141
|
+
} else if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
142
|
+
element.replaceChild(child, el);
|
|
143
|
+
} else if (child && typeof child === 'object') {
|
|
144
|
+
update(element.childNodes[i], child as VNode, isSvg);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remove extra old nodes
|
|
149
|
+
while (element.childNodes.length > len) {
|
|
150
|
+
element.removeChild(element.lastChild);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (new_len > len) {
|
|
154
|
+
const d = document.createDocumentFragment();
|
|
155
|
+
for (let i = len; i < children.length; i++) {
|
|
156
|
+
const child = children[i];
|
|
157
|
+
if (child != null) {
|
|
158
|
+
d.appendChild(create(child, isSvg));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
element.appendChild(d);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const safeHTML = (html: string) => {
|
|
166
|
+
const div = document.createElement('section');
|
|
167
|
+
div.insertAdjacentHTML('afterbegin', html)
|
|
168
|
+
return Array.from(div.children);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createText(node) {
|
|
172
|
+
if (node?.indexOf('_html:') === 0) { // ?
|
|
173
|
+
const div = document.createElement('div');
|
|
174
|
+
div.insertAdjacentHTML('afterbegin', node.substring(6))
|
|
175
|
+
return div;
|
|
176
|
+
} else {
|
|
177
|
+
return document.createTextNode(node ?? '');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function create(node: VNode | string | HTMLElement | SVGElement, isSvg: boolean): Element {
|
|
182
|
+
// console.assert(node !== null && node !== undefined);
|
|
183
|
+
if ((node instanceof HTMLElement) || (node instanceof SVGElement)) return node;
|
|
184
|
+
if (typeof node === "string") return createText(node);
|
|
185
|
+
|
|
186
|
+
// Type guard for VNode objects - handle invalid node types gracefully
|
|
187
|
+
if (!node || typeof node !== 'object' || !node.tag || (typeof node.tag === 'function')) {
|
|
188
|
+
return createText(typeof node === 'object' ? JSON.stringify(node) : String(node ?? ''));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
isSvg = isSvg || node.tag === "svg";
|
|
192
|
+
const element = isSvg
|
|
193
|
+
? document.createElementNS("http://www.w3.org/2000/svg", node.tag)
|
|
194
|
+
: document.createElement(node.tag);
|
|
195
|
+
|
|
196
|
+
updateProps(element, node.props, isSvg);
|
|
197
|
+
if (node.children) node.children.forEach(child => element.appendChild(create(child, isSvg)));
|
|
198
|
+
return element
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function mergeProps(oldProps: {}, newProps: {}): {} {
|
|
202
|
+
newProps['class'] = newProps['class'] || newProps['className'];
|
|
203
|
+
delete newProps['className'];
|
|
204
|
+
const props = {};
|
|
205
|
+
if (oldProps) Object.keys(oldProps).forEach(p => props[p] = null);
|
|
206
|
+
if (newProps) Object.keys(newProps).forEach(p => props[p] = newProps[p]);
|
|
207
|
+
return props;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function updateProps(element: Element, props: {}, isSvg) {
|
|
211
|
+
// console.assert(!!element);
|
|
212
|
+
const cached = element[ATTR_PROPS] || {};
|
|
213
|
+
props = mergeProps(cached, props || {});
|
|
214
|
+
element[ATTR_PROPS] = props;
|
|
215
|
+
|
|
216
|
+
for (const name in props) {
|
|
217
|
+
const value = props[name];
|
|
218
|
+
// if (cached[name] === value) continue;
|
|
219
|
+
// console.log('updateProps', name, value);
|
|
220
|
+
if (name.startsWith('data-')) {
|
|
221
|
+
const dname = name.substring(5);
|
|
222
|
+
const cname = dname.replace(/-(\w)/g, (match) => match[1].toUpperCase());
|
|
223
|
+
if (element.dataset[cname] !== value) {
|
|
224
|
+
if (value || value === "") element.dataset[cname] = value;
|
|
225
|
+
else delete element.dataset[cname];
|
|
226
|
+
}
|
|
227
|
+
} else if (name === 'style') {
|
|
228
|
+
if (element.style.cssText) element.style.cssText = '';
|
|
229
|
+
if (typeof value === 'string') element.style.cssText = value;
|
|
230
|
+
else {
|
|
231
|
+
for (const s in value) {
|
|
232
|
+
if (element.style[s] !== value[s]) element.style[s] = value[s];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else if (name.startsWith('xlink')) {
|
|
236
|
+
const xname = name.replace('xlink', '').toLowerCase();
|
|
237
|
+
if (value == null || value === false) {
|
|
238
|
+
element.removeAttributeNS('http://www.w3.org/1999/xlink', xname);
|
|
239
|
+
} else {
|
|
240
|
+
element.setAttributeNS('http://www.w3.org/1999/xlink', xname, value);
|
|
241
|
+
}
|
|
242
|
+
} else if (name.startsWith('on')) {
|
|
243
|
+
if (!value || typeof value === 'function') {
|
|
244
|
+
element[name] = value;
|
|
245
|
+
} else if (typeof value === 'string') {
|
|
246
|
+
if (value) element.setAttribute(name, value);
|
|
247
|
+
else element.removeAttribute(name);
|
|
248
|
+
}
|
|
249
|
+
} else if (/^id$|^class$|^list$|^readonly$|^contenteditable$|^role|-|^for$/g.test(name) || isSvg) {
|
|
250
|
+
if (element.getAttribute(name) !== value) {
|
|
251
|
+
if (value) element.setAttribute(name, value);
|
|
252
|
+
else element.removeAttribute(name);
|
|
253
|
+
}
|
|
254
|
+
} else if (element[name] !== value) {
|
|
255
|
+
element[name] = value;
|
|
256
|
+
}
|
|
257
|
+
if (name === 'key' && value !== undefined) {
|
|
258
|
+
keyCache[value] = element;
|
|
259
|
+
element.key = value; // Set key property on the DOM element
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (props && typeof props['ref'] === 'function') {
|
|
263
|
+
window.requestAnimationFrame(() => props['ref'](element));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function render_component(node, parent, idx) {
|
|
268
|
+
const { tag, props, children } = node;
|
|
269
|
+
let key = `_${idx}`;
|
|
270
|
+
let id = props && props['id'];
|
|
271
|
+
if (!id) id = `_${idx}${Date.now()}`;
|
|
272
|
+
else key = id;
|
|
273
|
+
let asTag = 'section';
|
|
274
|
+
if (props && props['as']) {
|
|
275
|
+
asTag = props['as'];
|
|
276
|
+
delete props['as'];
|
|
277
|
+
}
|
|
278
|
+
if (!parent.__componentCache) parent.__componentCache = {};
|
|
279
|
+
let component = parent.__componentCache[key];
|
|
280
|
+
if (!component || !(component instanceof tag) || !component.element) {
|
|
281
|
+
const element = document.createElement(asTag);
|
|
282
|
+
component = parent.__componentCache[key] = new tag({ ...props, children }).mount(element, { render: true });
|
|
283
|
+
} else {
|
|
284
|
+
component.renderState(component.state);
|
|
285
|
+
}
|
|
286
|
+
if (component.mounted) {
|
|
287
|
+
const new_state = component.mounted(props, children, component.state);
|
|
288
|
+
(typeof new_state !== 'undefined') && component.setState(new_state);
|
|
289
|
+
}
|
|
290
|
+
updateProps(component.element, props, false);
|
|
291
|
+
return component.element;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function createComponent(node, parent, idx = 0) {
|
|
295
|
+
if (typeof node === 'string') return node;
|
|
296
|
+
if (Array.isArray(node)) return node.map(child => createComponent(child, parent, idx++));
|
|
297
|
+
let vdom = node;
|
|
298
|
+
if (node && typeof node.tag === 'function' && Object.getPrototypeOf(node.tag).__isAppRunComponent) {
|
|
299
|
+
vdom = render_component(node, parent, idx);
|
|
300
|
+
}
|
|
301
|
+
if (vdom && Array.isArray(vdom.children)) {
|
|
302
|
+
const new_parent = vdom.props?._component;
|
|
303
|
+
if (new_parent) {
|
|
304
|
+
let i = 0;
|
|
305
|
+
vdom.children = vdom.children.map(child => createComponent(child, new_parent, i++));
|
|
306
|
+
} else {
|
|
307
|
+
vdom.children = vdom.children.map(child => createComponent(child, parent, idx++));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return vdom;
|
|
311
|
+
}
|