defuss-morph 0.1.0 โ†’ 0.2.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 CHANGED
@@ -15,11 +15,14 @@ Features:
15
15
  - ๐ŸŽ๏ธ Fast! Uses native DOM APIs
16
16
  - ๐ŸŽฏ Stable key/id-aware node matching
17
17
  - ๐Ÿ“ฆ CDN-served and packaged with ESM + CJS
18
- - ๐Ÿงน Event handler preservation w/ supports delegated event listeners
18
+ - ๐Ÿงน Event handler preservation; JSX handlers listen on the element itself, so propagation is exactly native (order, `stopPropagation()`, `currentTarget`, non-bubbling events like `toggle`, `load`, `error`)
19
19
  - ๐Ÿ“ Form-state preservation (see one limitation below w/ solution)
20
+ - ๐Ÿ’ฌ HTML comments are kept and morphed like text โ€” `morph(el, el.innerHTML)` changes no node
20
21
  - โšก No compile/transpile step required (but optionally available)
21
22
  - ๐Ÿชถ Extremely lightweight โ€” just ~6 KiB minified and gzipped
22
23
  - ๐Ÿ’… Includes support for beautiful transitions (fade, slide, shake, custom styles)
24
+ - ๐Ÿงฉ Supports partial updates (diff mode) for streaming/AI-driven UIs
25
+ - โœ… ~97%+ test coverage with unit tests and real browser E2E tests
23
26
  - ๐ŸŸฆ Written in TypeScript
24
27
 
25
28
  ## Quick, traditional CDN-based setup
@@ -163,7 +166,8 @@ Morphs the children of `el` to match the given HTML string or JSX/VNode content.
163
166
  `morph()` accepts **either an HTML string or JSX / VNodes** โ€” same API, same
164
167
  algorithm. Strings are parsed with the native `DOMParser`; VNodes
165
168
  (`{ type, attributes, children }` objects, as produced by any JSX factory)
166
- are morphed directly:
169
+ are morphed directly. An HTML comment is `{ type: "#comment", value }`
170
+ (`COMMENT_TYPE`):
167
171
 
168
172
  ```tsx
169
173
  import { morph } from "defuss-morph";
@@ -243,6 +247,8 @@ anything, so addressing stays unambiguous:
243
247
  - A declared `children` replaces the node's children; if children are not
244
248
  declared, they stay untouched (via VNodes, `children: []` clears explicitly).
245
249
  - A tag change on an addressed node applies as an in-place replacement.
250
+ - Top-level comments in a change-set are skipped (they have no address, and
251
+ appending would duplicate them per patch); comments *inside* items morph.
246
252
 
247
253
  Great for streaming/AI-driven UIs: patch one table row or status badge without
248
254
  re-sending (or risking) the rest of the subtree. Runnable demo:
@@ -276,6 +282,8 @@ import {
276
282
  domNodeToVNode, // DOM Node -> VNode
277
283
  getRenderer, // VNode -> DOM renderer factory
278
284
  registerDelegatedEvent, removeDelegatedEvent, clearDelegatedEventsDeep, // ...
285
+ onClearDelegatedEvents, // hook: tear down your own native listeners whenever
286
+ // morph removes or replaces an element
279
287
  } from "defuss-morph";
280
288
 
281
289
  // the third `globals` argument is optional everywhere โ€” it is derived
@@ -353,10 +361,10 @@ The same rule applies to an `<input>`'s live `value`. See the runnable demos [`e
353
361
  <!-- bundle-size:start -->
354
362
  | File | Size | Gzipped | Purpose |
355
363
  | --- | ---: | ---: | --- |
356
- | `index.mjs` | 40.2 kB | 9.5 kB | ESM/library build; used when installing via npm/bun |
357
- | `index.cjs` | 41.2 kB | 9.7 kB | CommonJS build |
358
- | `all.js` | 41.6 kB | 9.8 kB | UMD build; for CDN-based usage with debugging |
359
- | `all.min.js` | 18.9 kB | **6.8 kB** | Minified UMD build; for CDN-based usage without debugging (Pareto-optimal when no bundler is used) |
364
+ | `index.mjs` | 39.3 kB | 9.6 kB | ESM/library build; used when installing via npm/bun |
365
+ | `index.cjs` | 40.4 kB | 9.7 kB | CommonJS build |
366
+ | `all.js` | 40.9 kB | 9.9 kB | UMD build; for CDN-based usage with debugging |
367
+ | `all.min.js` | 18.7 kB | **6.8 kB** | Minified UMD build; for CDN-based usage without debugging (Pareto-optimal when no bundler is used) |
360
368
  <!-- bundle-size:end -->
361
369
 
362
370
  `index.cjs` (CommonJS) is kept for `require()` compatibility on older tool
package/dist/all.js CHANGED
@@ -27,12 +27,14 @@ __export(exports_src, {
27
27
  performTransition: () => performTransition,
28
28
  parseEventPropName: () => parseEventPropName,
29
29
  parseDOM: () => parseDOM,
30
+ onClearDelegatedEvents: () => onClearDelegatedEvents,
30
31
  observeUnmount: () => observeUnmount,
31
32
  nsMap: () => nsMap,
32
33
  morph: () => morph2,
33
34
  isSVG: () => isSVG,
34
35
  isMarkup: () => isMarkup,
35
36
  isHTML: () => isHTML,
37
+ isCommentVNode: () => isCommentVNode,
36
38
  htmlStringToVNodes: () => htmlStringToVNodes,
37
39
  handleLifecycleEventsForOnMount: () => handleLifecycleEventsForOnMount,
38
40
  getTransitionStyles: () => getTransitionStyles,
@@ -51,6 +53,7 @@ __export(exports_src, {
51
53
  FROM_DOM_MARKER: () => FROM_DOM_MARKER,
52
54
  DEFAULT_TRANSITION_CONFIG: () => DEFAULT_TRANSITION_CONFIG,
53
55
  DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE: () => DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE,
56
+ COMMENT_TYPE: () => COMMENT_TYPE,
54
57
  CLASS_ATTRIBUTE_NAME: () => CLASS_ATTRIBUTE_NAME,
55
58
  CAPTURE_ONLY_EVENTS: () => CAPTURE_ONLY_EVENTS
56
59
  });
@@ -67,8 +70,7 @@ var CAPTURE_ONLY_EVENTS = new Set([
67
70
  "mouseleave"
68
71
  ]);
69
72
  var elementHandlerMap = new WeakMap;
70
- var bubbleDispatched = new WeakMap;
71
- var captureDispatched = new WeakMap;
73
+ var slotListeners = new WeakMap;
72
74
  var activeDispatches = new WeakMap;
73
75
  var parseEventPropName = (propName) => {
74
76
  if (!propName.startsWith("on"))
@@ -91,146 +93,66 @@ var getOrCreateElementHandlers = (el) => {
91
93
  elementHandlerMap.set(el, created);
92
94
  return created;
93
95
  };
94
- var getEventPath = (event) => {
95
- const composedPath = event.composedPath?.();
96
- if (composedPath && composedPath.length > 0)
97
- return composedPath;
98
- const path = [];
99
- let node = event.target;
100
- while (node) {
101
- path.push(node);
102
- const maybeNode = node;
103
- if (typeof maybeNode === "object" && maybeNode && "parentNode" in maybeNode) {
104
- node = maybeNode.parentNode;
105
- continue;
106
- }
107
- break;
96
+ var runSlot = (target, eventType, phase, event) => {
97
+ const handler = elementHandlerMap.get(target)?.get(eventType)?.[phase];
98
+ if (!handler)
99
+ return;
100
+ const dispatchKey = `${eventType}:${phase}`;
101
+ let active = activeDispatches.get(target);
102
+ if (active?.has(dispatchKey))
103
+ return;
104
+ if (!active) {
105
+ active = new Set;
106
+ activeDispatches.set(target, active);
107
+ }
108
+ active.add(dispatchKey);
109
+ try {
110
+ handler.call(target, event);
111
+ } finally {
112
+ active.delete(dispatchKey);
108
113
  }
109
- const doc = event.target?.ownerDocument;
110
- if (doc && path[path.length - 1] !== doc)
111
- path.push(doc);
112
- const win = doc?.defaultView;
113
- if (win && path[path.length - 1] !== win)
114
- path.push(win);
115
- return path;
116
114
  };
117
- var createPhaseHandler = (eventType, phase) => {
118
- const dispatched = phase === "capture" ? captureDispatched : bubbleDispatched;
119
- return (event) => {
120
- const path = getEventPath(event).filter((t) => typeof t === "object" && t !== null && t.nodeType === 1);
121
- const ordered = phase === "capture" ? [...path].reverse() : path;
122
- for (const target of ordered) {
123
- const handlersByEvent = elementHandlerMap.get(target);
124
- if (!handlersByEvent)
125
- continue;
126
- const entry = handlersByEvent.get(eventType);
127
- if (!entry)
128
- continue;
129
- let targets = dispatched.get(event);
130
- if (targets?.has(target))
131
- continue;
132
- if (!targets) {
133
- targets = new WeakSet;
134
- dispatched.set(event, targets);
135
- }
136
- targets.add(target);
137
- const dispatchKey = `${eventType}:${phase}`;
138
- let activeSet = activeDispatches.get(target);
139
- if (activeSet?.has(dispatchKey))
140
- continue;
141
- if (!activeSet) {
142
- activeSet = new Set;
143
- activeDispatches.set(target, activeSet);
144
- }
145
- activeSet.add(dispatchKey);
146
- try {
147
- if (phase === "capture") {
148
- if (entry.capture) {
149
- entry.capture.call(target, event);
150
- if (event.cancelBubble)
151
- return;
152
- }
153
- if (entry.captureSet) {
154
- for (const handler of entry.captureSet) {
155
- handler.call(target, event);
156
- if (event.cancelBubble)
157
- return;
158
- }
159
- }
160
- } else {
161
- if (entry.bubble) {
162
- entry.bubble.call(target, event);
163
- if (event.cancelBubble)
164
- return;
165
- }
166
- if (entry.bubbleSet) {
167
- for (const handler of entry.bubbleSet) {
168
- handler.call(target, event);
169
- if (event.cancelBubble)
170
- return;
171
- }
172
- }
173
- }
174
- } finally {
175
- activeSet.delete(dispatchKey);
176
- }
115
+ var setSlot = (target, eventType, phase, entry, handler) => {
116
+ entry[phase] = handler;
117
+ const key = `${eventType}:${phase}`;
118
+ const capture = phase === "capture";
119
+ let byKey = slotListeners.get(target);
120
+ const installed = byKey?.get(key);
121
+ if (handler && !installed) {
122
+ if (!byKey) {
123
+ byKey = new Map;
124
+ slotListeners.set(target, byKey);
177
125
  }
178
- };
179
- };
180
- var installedRootListeners = new WeakMap;
181
- var ensureRootListener = (root, eventType) => {
182
- const installed = installedRootListeners.get(root) ?? new Set;
183
- installedRootListeners.set(root, installed);
184
- const captureKey = `${eventType}:capture`;
185
- if (!installed.has(captureKey)) {
186
- root.addEventListener(eventType, createPhaseHandler(eventType, "capture"), true);
187
- installed.add(captureKey);
188
- }
189
- const bubbleKey = `${eventType}:bubble`;
190
- if (!installed.has(bubbleKey)) {
191
- root.addEventListener(eventType, createPhaseHandler(eventType, "bubble"), false);
192
- installed.add(bubbleKey);
126
+ const trampoline = (event) => runSlot(target, eventType, phase, event);
127
+ byKey.set(key, trampoline);
128
+ target.addEventListener(eventType, trampoline, capture);
129
+ } else if (!handler && installed) {
130
+ byKey.delete(key);
131
+ target.removeEventListener(eventType, installed, capture);
193
132
  }
194
133
  };
195
- var getEventRoot = (element) => {
196
- const root = element.getRootNode();
197
- if (root && root.nodeType === 9) {
198
- return root;
199
- }
200
- if (root && root.nodeType === 11 && "host" in root) {
201
- return root;
202
- }
203
- return null;
134
+ var clearPhase = (target, eventType, entry, phase) => {
135
+ setSlot(target, eventType, phase, entry, undefined);
136
+ const setKey = phase === "capture" ? "captureSet" : "bubbleSet";
137
+ for (const handler of entry[setKey] ?? [])
138
+ target.removeEventListener(eventType, handler, phase === "capture");
139
+ entry[setKey] = undefined;
204
140
  };
205
141
  var registerDelegatedEvent = (element, eventType, handler, options = {}) => {
206
- const root = getEventRoot(element);
207
142
  const capture = options.capture || CAPTURE_ONLY_EVENTS.has(eventType);
208
- if (root) {
209
- ensureRootListener(root, eventType);
210
- } else if (element.ownerDocument) {
211
- ensureRootListener(element.ownerDocument, eventType);
212
- } else {
213
- element.addEventListener(eventType, handler, capture);
214
- }
143
+ const phase = capture ? "capture" : "bubble";
215
144
  const byEvent = getOrCreateElementHandlers(element);
216
145
  const entry = byEvent.get(eventType) ?? {};
217
146
  byEvent.set(eventType, entry);
218
147
  if (options.multi) {
219
- if (capture) {
220
- if (!entry.captureSet)
221
- entry.captureSet = new Set;
222
- entry.captureSet.add(handler);
223
- } else {
224
- if (!entry.bubbleSet)
225
- entry.bubbleSet = new Set;
226
- entry.bubbleSet.add(handler);
148
+ const setKey = capture ? "captureSet" : "bubbleSet";
149
+ const set = entry[setKey] ??= new Set;
150
+ if (!set.has(handler)) {
151
+ set.add(handler);
152
+ element.addEventListener(eventType, handler, capture);
227
153
  }
228
154
  } else {
229
- if (capture) {
230
- entry.capture = handler;
231
- } else {
232
- entry.bubble = handler;
233
- }
155
+ setSlot(element, eventType, phase, entry, handler);
234
156
  }
235
157
  };
236
158
  var isEntryEmpty = (entry) => !entry.capture && !entry.bubble && (!entry.captureSet || entry.captureSet.size === 0) && (!entry.bubbleSet || entry.bubbleSet.size === 0);
@@ -242,35 +164,39 @@ var removeDelegatedEvent = (target, eventType, handler, _options = {}) => {
242
164
  if (!entry)
243
165
  return;
244
166
  if (handler) {
245
- if (entry.captureSet) {
246
- entry.captureSet.delete(handler);
247
- }
248
- if (entry.bubbleSet) {
249
- entry.bubbleSet.delete(handler);
167
+ for (const phase of ["capture", "bubble"]) {
168
+ const set = phase === "capture" ? entry.captureSet : entry.bubbleSet;
169
+ if (set?.delete(handler))
170
+ target.removeEventListener(eventType, handler, phase === "capture");
171
+ if (entry[phase] === handler)
172
+ setSlot(target, eventType, phase, entry, undefined);
250
173
  }
251
- if (entry.capture === handler) {
252
- entry.capture = undefined;
253
- }
254
- if (entry.bubble === handler) {
255
- entry.bubble = undefined;
256
- }
257
- target.removeEventListener(eventType, handler, true);
258
- target.removeEventListener(eventType, handler, false);
259
174
  } else {
260
- entry.capture = undefined;
261
- entry.bubble = undefined;
262
- entry.captureSet = undefined;
263
- entry.bubbleSet = undefined;
175
+ clearPhase(target, eventType, entry, "capture");
176
+ clearPhase(target, eventType, entry, "bubble");
264
177
  }
265
178
  if (isEntryEmpty(entry)) {
266
179
  byEvent.delete(eventType);
267
180
  }
268
181
  };
182
+ var clearHooks = new Set;
183
+ var onClearDelegatedEvents = (hook) => {
184
+ clearHooks.add(hook);
185
+ return () => {
186
+ clearHooks.delete(hook);
187
+ };
188
+ };
269
189
  var clearDelegatedEvents = (target) => {
270
190
  const byEvent = elementHandlerMap.get(target);
271
- if (!byEvent)
272
- return;
273
- byEvent.clear();
191
+ if (byEvent) {
192
+ for (const [eventType, entry] of byEvent) {
193
+ clearPhase(target, eventType, entry, "capture");
194
+ clearPhase(target, eventType, entry, "bubble");
195
+ }
196
+ byEvent.clear();
197
+ }
198
+ for (const hook of clearHooks)
199
+ hook(target);
274
200
  };
275
201
  var clearDelegatedEventsDeep = (root) => {
276
202
  clearDelegatedEvents(root);
@@ -310,17 +236,110 @@ var removeDelegatedEventByKey = (element, eventType, phase) => {
310
236
  const entry = byEvent.get(eventType);
311
237
  if (!entry)
312
238
  return;
313
- if (phase === "capture") {
314
- entry.capture = undefined;
315
- entry.captureSet = undefined;
316
- } else {
317
- entry.bubble = undefined;
318
- entry.bubbleSet = undefined;
319
- }
239
+ clearPhase(element, eventType, entry, phase);
320
240
  if (isEntryEmpty(entry))
321
241
  byEvent.delete(eventType);
322
242
  };
323
243
 
244
+ // src/html.ts
245
+ var FROM_DOM_MARKER = Symbol("defuss-morph.from-dom");
246
+ var COMMENT_TYPE = "#comment";
247
+ var isCommentVNode = (value) => !!value && typeof value === "object" && value.type === COMMENT_TYPE;
248
+ var HTML_BOOLEAN_ATTRIBUTES = new Set([
249
+ "allowfullscreen",
250
+ "async",
251
+ "autofocus",
252
+ "autoplay",
253
+ "checked",
254
+ "controls",
255
+ "default",
256
+ "defer",
257
+ "disabled",
258
+ "formnovalidate",
259
+ "hidden",
260
+ "inert",
261
+ "ismap",
262
+ "itemscope",
263
+ "loop",
264
+ "multiple",
265
+ "muted",
266
+ "nomodule",
267
+ "novalidate",
268
+ "open",
269
+ "playsinline",
270
+ "readonly",
271
+ "required",
272
+ "reversed",
273
+ "selected"
274
+ ]);
275
+ var domAttributeToVNodeValue = (attr) => HTML_BOOLEAN_ATTRIBUTES.has(attr.name.toLowerCase()) ? true : attr.value;
276
+ function parseDOM(input, type, Parser) {
277
+ return new Parser().parseFromString(input, type);
278
+ }
279
+ function isSVG(input, Parser) {
280
+ const doc = parseDOM(input, "image/svg+xml", Parser);
281
+ if (!doc.documentElement)
282
+ return false;
283
+ return doc.documentElement.nodeName.toLowerCase() === "svg";
284
+ }
285
+ function isHTML(input, Parser) {
286
+ const doc = parseDOM(input, "text/html", Parser);
287
+ return doc.documentElement.querySelectorAll("*").length > 2;
288
+ }
289
+ var isMarkup = (input, Parser) => input.indexOf("<") > -1 && input.indexOf(">") > -1 && (isHTML(input, Parser) || isSVG(input, Parser));
290
+ function renderMarkup(markup, Parser, doc) {
291
+ const parsed = doc ? doc : parseDOM(markup, getMimeType(markup, Parser), Parser);
292
+ if (parsed.body)
293
+ return Array.from(parsed.body.childNodes);
294
+ return parsed.documentElement ? [parsed.documentElement] : [];
295
+ }
296
+ function getMimeType(input, Parser) {
297
+ if (isSVG(input, Parser)) {
298
+ return "image/svg+xml";
299
+ }
300
+ return "text/html";
301
+ }
302
+ function domNodeToVNode(node) {
303
+ if (node.nodeType === 3) {
304
+ return node.textContent || "";
305
+ }
306
+ if (node.nodeType === 1) {
307
+ const element = node;
308
+ const attributes = {};
309
+ for (let i = 0;i < element.attributes.length; i++) {
310
+ const attr = element.attributes[i];
311
+ attributes[attr.name] = domAttributeToVNodeValue(attr);
312
+ }
313
+ const children = [];
314
+ for (let i = 0;i < element.childNodes.length; i++) {
315
+ const childVNode = domNodeToVNode(element.childNodes[i]);
316
+ children.push(childVNode);
317
+ }
318
+ return {
319
+ type: element.tagName.toLowerCase(),
320
+ attributes: { ...attributes, [FROM_DOM_MARKER]: true },
321
+ children
322
+ };
323
+ }
324
+ if (node.nodeType === 8) {
325
+ return { type: COMMENT_TYPE, value: node.nodeValue ?? "" };
326
+ }
327
+ return "";
328
+ }
329
+ var DOCUMENT_START = /^\s*(?:<!--[\s\S]*?-->\s*)*<(?:!doctype|html|head|body)[\s>/]/i;
330
+ function htmlStringToVNodes(html, Parser) {
331
+ const parser = new Parser;
332
+ const doc = parser.parseFromString(DOCUMENT_START.test(html) ? html : `<body>${html}`, "text/html");
333
+ const vNodes = [];
334
+ for (let i = 0;i < doc.body.childNodes.length; i++) {
335
+ const vnode = domNodeToVNode(doc.body.childNodes[i]);
336
+ if (vnode !== "") {
337
+ vNodes.push(vnode);
338
+ }
339
+ }
340
+ return vNodes;
341
+ }
342
+
324
343
  // src/renderer.ts
325
344
  var CLASS_ATTRIBUTE_NAME = "class";
326
345
  var XLINK_ATTRIBUTE_NAME = "xlink";
@@ -393,6 +412,11 @@ var getRenderer = (document2) => {
393
412
  },
394
413
  createElement: (virtualNode, parentDomElement) => {
395
414
  let newEl;
415
+ if (isCommentVNode(virtualNode)) {
416
+ const comment = document2.createComment(virtualNode.value ?? "");
417
+ parentDomElement?.appendChild(comment);
418
+ return comment;
419
+ }
396
420
  try {
397
421
  if (typeof virtualNode === "function" && virtualNode.constructor.name === "AsyncFunction") {
398
422
  newEl = document2.createElement("div");
@@ -548,99 +572,6 @@ var getRenderer = (document2) => {
548
572
  return renderer;
549
573
  };
550
574
 
551
- // src/html.ts
552
- var FROM_DOM_MARKER = Symbol("defuss-morph.from-dom");
553
- var HTML_BOOLEAN_ATTRIBUTES = new Set([
554
- "allowfullscreen",
555
- "async",
556
- "autofocus",
557
- "autoplay",
558
- "checked",
559
- "controls",
560
- "default",
561
- "defer",
562
- "disabled",
563
- "formnovalidate",
564
- "hidden",
565
- "inert",
566
- "ismap",
567
- "itemscope",
568
- "loop",
569
- "multiple",
570
- "muted",
571
- "nomodule",
572
- "novalidate",
573
- "open",
574
- "playsinline",
575
- "readonly",
576
- "required",
577
- "reversed",
578
- "selected"
579
- ]);
580
- var domAttributeToVNodeValue = (attr) => HTML_BOOLEAN_ATTRIBUTES.has(attr.name.toLowerCase()) ? true : attr.value;
581
- function parseDOM(input, type, Parser) {
582
- return new Parser().parseFromString(input, type);
583
- }
584
- function isSVG(input, Parser) {
585
- const doc = parseDOM(input, "image/svg+xml", Parser);
586
- if (!doc.documentElement)
587
- return false;
588
- return doc.documentElement.nodeName.toLowerCase() === "svg";
589
- }
590
- function isHTML(input, Parser) {
591
- const doc = parseDOM(input, "text/html", Parser);
592
- return doc.documentElement.querySelectorAll("*").length > 2;
593
- }
594
- var isMarkup = (input, Parser) => input.indexOf("<") > -1 && input.indexOf(">") > -1 && (isHTML(input, Parser) || isSVG(input, Parser));
595
- function renderMarkup(markup, Parser, doc) {
596
- const parsed = doc ? doc : parseDOM(markup, getMimeType(markup, Parser), Parser);
597
- if (parsed.body)
598
- return Array.from(parsed.body.childNodes);
599
- return parsed.documentElement ? [parsed.documentElement] : [];
600
- }
601
- function getMimeType(input, Parser) {
602
- if (isSVG(input, Parser)) {
603
- return "image/svg+xml";
604
- }
605
- return "text/html";
606
- }
607
- function domNodeToVNode(node) {
608
- if (node.nodeType === 3) {
609
- return node.textContent || "";
610
- }
611
- if (node.nodeType === 1) {
612
- const element = node;
613
- const attributes = {};
614
- for (let i = 0;i < element.attributes.length; i++) {
615
- const attr = element.attributes[i];
616
- attributes[attr.name] = domAttributeToVNodeValue(attr);
617
- }
618
- const children = [];
619
- for (let i = 0;i < element.childNodes.length; i++) {
620
- const childVNode = domNodeToVNode(element.childNodes[i]);
621
- children.push(childVNode);
622
- }
623
- return {
624
- type: element.tagName.toLowerCase(),
625
- attributes: { ...attributes, [FROM_DOM_MARKER]: true },
626
- children
627
- };
628
- }
629
- return "";
630
- }
631
- function htmlStringToVNodes(html, Parser) {
632
- const parser = new Parser;
633
- const doc = parser.parseFromString(html, "text/html");
634
- const vNodes = [];
635
- for (let i = 0;i < doc.body.childNodes.length; i++) {
636
- const vnode = domNodeToVNode(doc.body.childNodes[i]);
637
- if (vnode !== "") {
638
- vNodes.push(vnode);
639
- }
640
- }
641
- return vNodes;
642
- }
643
-
644
575
  // src/morph.ts
645
576
  var areDomNodesEqual = (oldNode, newNode) => {
646
577
  if (oldNode === newNode)
@@ -663,8 +594,8 @@ var areDomNodesEqual = (oldNode, newNode) => {
663
594
  return false;
664
595
  }
665
596
  }
666
- if (oldNode.nodeType === 3) {
667
- if (oldNode.textContent !== newNode.textContent)
597
+ if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
598
+ if (oldNode.nodeValue !== newNode.nodeValue)
668
599
  return false;
669
600
  }
670
601
  return true;
@@ -761,6 +692,8 @@ function areNodeAndChildMatching(domNode, child) {
761
692
  if (typeof child === "string" || typeof child === "number" || typeof child === "boolean") {
762
693
  return domNode.nodeType === 3;
763
694
  }
695
+ if (isCommentVNode(child))
696
+ return domNode.nodeType === 8;
764
697
  if (child && typeof child === "object") {
765
698
  if (domNode.nodeType !== 1)
766
699
  return false;
@@ -854,6 +787,11 @@ function patchElementInPlace(el, vnode, globals, mergeAttributes = false) {
854
787
  return;
855
788
  morphDomDirect(el, vnode.children ?? [], globals);
856
789
  }
790
+ function replaceNode(old, next) {
791
+ if (old.nodeType === 1)
792
+ clearDelegatedEventsDeep(old);
793
+ old.parentNode?.replaceChild(next, old);
794
+ }
857
795
  function morphNode(domNode, child, globals, mergeAttributes = false) {
858
796
  if (typeof child === "string" || typeof child === "number" || typeof child === "boolean") {
859
797
  const text = String(child);
@@ -863,7 +801,18 @@ function morphNode(domNode, child, globals, mergeAttributes = false) {
863
801
  return domNode;
864
802
  }
865
803
  const next = globals.window.document.createTextNode(text);
866
- domNode.parentNode?.replaceChild(next, domNode);
804
+ replaceNode(domNode, next);
805
+ return next;
806
+ }
807
+ if (isCommentVNode(child)) {
808
+ const data = child.value ?? "";
809
+ if (domNode.nodeType === 8) {
810
+ if (domNode.nodeValue !== data)
811
+ domNode.nodeValue = data;
812
+ return domNode;
813
+ }
814
+ const next = globals.window.document.createComment(data);
815
+ replaceNode(domNode, next);
867
816
  return next;
868
817
  }
869
818
  if (child && typeof child === "object") {
@@ -887,13 +836,15 @@ function morphNode(domNode, child, globals, mergeAttributes = false) {
887
836
  const first = Array.isArray(created) ? created[0] : created;
888
837
  if (!first)
889
838
  return null;
890
- el.parentNode?.replaceChild(first, el);
839
+ replaceNode(el, first);
891
840
  handleLifecycleEventsForOnMount(first);
892
841
  return first;
893
842
  }
894
843
  patchElementInPlace(el, child, globals, mergeAttributes);
895
844
  return el;
896
845
  }
846
+ if (domNode.nodeType === 1)
847
+ clearDelegatedEventsDeep(domNode);
897
848
  domNode.parentNode?.removeChild(domNode);
898
849
  return null;
899
850
  }
@@ -954,6 +905,8 @@ function morphDiff(targetRoot, patchItems, globals) {
954
905
  for (const item of patchItems) {
955
906
  if (typeof item === "string" && item.trim() === "")
956
907
  continue;
908
+ if (isCommentVNode(item))
909
+ continue;
957
910
  const key = getVNodeMatchKey(item);
958
911
  if (!key) {
959
912
  throw new Error(`morph diff: patch items must be elements with a key or id attribute (got ${describePatchItem(item)})`);
@@ -1263,7 +1216,8 @@ var morph2 = (el, newContent, options = {}) => {
1263
1216
  // src/global.ts
1264
1217
  if (typeof globalThis !== "undefined") {
1265
1218
  const g = globalThis;
1266
- if (typeof g.df$ !== "object" || g.df$ === null) {
1219
+ const current = g.df$;
1220
+ if (current === null || typeof current !== "object" && typeof current !== "function") {
1267
1221
  g.df$ = {};
1268
1222
  }
1269
1223
  Object.assign(g.df$, exports_src);
@@ -1280,12 +1234,14 @@ export {
1280
1234
  performTransition,
1281
1235
  parseEventPropName,
1282
1236
  parseDOM,
1237
+ onClearDelegatedEvents,
1283
1238
  observeUnmount,
1284
1239
  nsMap,
1285
1240
  morph2 as morph,
1286
1241
  isSVG,
1287
1242
  isMarkup,
1288
1243
  isHTML,
1244
+ isCommentVNode,
1289
1245
  htmlStringToVNodes,
1290
1246
  handleLifecycleEventsForOnMount,
1291
1247
  getTransitionStyles,
@@ -1304,9 +1260,10 @@ export {
1304
1260
  FROM_DOM_MARKER,
1305
1261
  DEFAULT_TRANSITION_CONFIG,
1306
1262
  DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE,
1263
+ COMMENT_TYPE,
1307
1264
  CLASS_ATTRIBUTE_NAME,
1308
1265
  CAPTURE_ONLY_EVENTS
1309
1266
  };
1310
1267
 
1311
- //# debugId=6F130A0428D5249264756E2164756E21
1268
+ //# debugId=2B212A8922F1193364756E2164756E21
1312
1269
  //# sourceMappingURL=all.js.map