defuss-morph 0.1.1 → 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/dist/index.cjs CHANGED
@@ -11,8 +11,7 @@ const CAPTURE_ONLY_EVENTS = /* @__PURE__ */ new Set([
11
11
  // Note: focusin/focusout DO bubble, so they're not included here
12
12
  ]);
13
13
  const elementHandlerMap = /* @__PURE__ */ new WeakMap();
14
- const bubbleDispatched = /* @__PURE__ */ new WeakMap();
15
- const captureDispatched = /* @__PURE__ */ new WeakMap();
14
+ const slotListeners = /* @__PURE__ */ new WeakMap();
16
15
  const activeDispatches = /* @__PURE__ */ new WeakMap();
17
16
  const parseEventPropName = (propName) => {
18
17
  if (!propName.startsWith("on")) return null;
@@ -31,147 +30,64 @@ const getOrCreateElementHandlers = (el) => {
31
30
  elementHandlerMap.set(el, created);
32
31
  return created;
33
32
  };
34
- const getEventPath = (event) => {
35
- const composedPath = event.composedPath?.();
36
- if (composedPath && composedPath.length > 0) return composedPath;
37
- const path = [];
38
- let node = event.target;
39
- while (node) {
40
- path.push(node);
41
- const maybeNode = node;
42
- if (typeof maybeNode === "object" && maybeNode && "parentNode" in maybeNode) {
43
- node = maybeNode.parentNode;
44
- continue;
45
- }
46
- break;
33
+ const runSlot = (target, eventType, phase, event) => {
34
+ const handler = elementHandlerMap.get(target)?.get(eventType)?.[phase];
35
+ if (!handler) return;
36
+ const dispatchKey = `${eventType}:${phase}`;
37
+ let active = activeDispatches.get(target);
38
+ if (active?.has(dispatchKey)) return;
39
+ if (!active) {
40
+ active = /* @__PURE__ */ new Set();
41
+ activeDispatches.set(target, active);
42
+ }
43
+ active.add(dispatchKey);
44
+ try {
45
+ handler.call(target, event);
46
+ } finally {
47
+ active.delete(dispatchKey);
47
48
  }
48
- const doc = event.target?.ownerDocument;
49
- if (doc && path[path.length - 1] !== doc) path.push(doc);
50
- const win = doc?.defaultView;
51
- if (win && path[path.length - 1] !== win) path.push(win);
52
- return path;
53
49
  };
54
- const createPhaseHandler = (eventType, phase) => {
55
- const dispatched = phase === "capture" ? captureDispatched : bubbleDispatched;
56
- return (event) => {
57
- const path = getEventPath(event).filter(
58
- (t) => typeof t === "object" && t !== null && t.nodeType === 1
59
- );
60
- const ordered = phase === "capture" ? [...path].reverse() : path;
61
- for (const target of ordered) {
62
- const handlersByEvent = elementHandlerMap.get(target);
63
- if (!handlersByEvent) continue;
64
- const entry = handlersByEvent.get(eventType);
65
- if (!entry) continue;
66
- let targets = dispatched.get(event);
67
- if (targets?.has(target)) continue;
68
- if (!targets) {
69
- targets = /* @__PURE__ */ new WeakSet();
70
- dispatched.set(event, targets);
71
- }
72
- targets.add(target);
73
- const dispatchKey = `${eventType}:${phase}`;
74
- let activeSet = activeDispatches.get(target);
75
- if (activeSet?.has(dispatchKey)) continue;
76
- if (!activeSet) {
77
- activeSet = /* @__PURE__ */ new Set();
78
- activeDispatches.set(target, activeSet);
79
- }
80
- activeSet.add(dispatchKey);
81
- try {
82
- if (phase === "capture") {
83
- if (entry.capture) {
84
- entry.capture.call(target, event);
85
- if (event.cancelBubble)
86
- return;
87
- }
88
- if (entry.captureSet) {
89
- for (const handler of entry.captureSet) {
90
- handler.call(target, event);
91
- if (event.cancelBubble)
92
- return;
93
- }
94
- }
95
- } else {
96
- if (entry.bubble) {
97
- entry.bubble.call(target, event);
98
- if (event.cancelBubble)
99
- return;
100
- }
101
- if (entry.bubbleSet) {
102
- for (const handler of entry.bubbleSet) {
103
- handler.call(target, event);
104
- if (event.cancelBubble)
105
- return;
106
- }
107
- }
108
- }
109
- } finally {
110
- activeSet.delete(dispatchKey);
111
- }
50
+ const setSlot = (target, eventType, phase, entry, handler) => {
51
+ entry[phase] = handler;
52
+ const key = `${eventType}:${phase}`;
53
+ const capture = phase === "capture";
54
+ let byKey = slotListeners.get(target);
55
+ const installed = byKey?.get(key);
56
+ if (handler && !installed) {
57
+ if (!byKey) {
58
+ byKey = /* @__PURE__ */ new Map();
59
+ slotListeners.set(target, byKey);
112
60
  }
113
- };
114
- };
115
- const installedRootListeners = /* @__PURE__ */ new WeakMap();
116
- const ensureRootListener = (root, eventType) => {
117
- const installed = installedRootListeners.get(root) ?? /* @__PURE__ */ new Set();
118
- installedRootListeners.set(root, installed);
119
- const captureKey = `${eventType}:capture`;
120
- if (!installed.has(captureKey)) {
121
- root.addEventListener(
122
- eventType,
123
- createPhaseHandler(eventType, "capture"),
124
- true
125
- );
126
- installed.add(captureKey);
127
- }
128
- const bubbleKey = `${eventType}:bubble`;
129
- if (!installed.has(bubbleKey)) {
130
- root.addEventListener(
131
- eventType,
132
- createPhaseHandler(eventType, "bubble"),
133
- false
134
- );
135
- installed.add(bubbleKey);
61
+ const trampoline = (event) => runSlot(target, eventType, phase, event);
62
+ byKey.set(key, trampoline);
63
+ target.addEventListener(eventType, trampoline, capture);
64
+ } else if (!handler && installed) {
65
+ byKey.delete(key);
66
+ target.removeEventListener(eventType, installed, capture);
136
67
  }
137
68
  };
138
- const getEventRoot = (element) => {
139
- const root = element.getRootNode();
140
- if (root && root.nodeType === 9) {
141
- return root;
142
- }
143
- if (root && root.nodeType === 11 && "host" in root) {
144
- return root;
145
- }
146
- return null;
69
+ const clearPhase = (target, eventType, entry, phase) => {
70
+ setSlot(target, eventType, phase, entry, void 0);
71
+ const setKey = phase === "capture" ? "captureSet" : "bubbleSet";
72
+ for (const handler of entry[setKey] ?? [])
73
+ target.removeEventListener(eventType, handler, phase === "capture");
74
+ entry[setKey] = void 0;
147
75
  };
148
76
  const registerDelegatedEvent = (element, eventType, handler, options = {}) => {
149
- const root = getEventRoot(element);
150
77
  const capture = options.capture || CAPTURE_ONLY_EVENTS.has(eventType);
151
- if (root) {
152
- ensureRootListener(root, eventType);
153
- } else if (element.ownerDocument) {
154
- ensureRootListener(element.ownerDocument, eventType);
155
- } else {
156
- element.addEventListener(eventType, handler, capture);
157
- }
78
+ const phase = capture ? "capture" : "bubble";
158
79
  const byEvent = getOrCreateElementHandlers(element);
159
80
  const entry = byEvent.get(eventType) ?? {};
160
81
  byEvent.set(eventType, entry);
161
82
  if (options.multi) {
162
- if (capture) {
163
- if (!entry.captureSet) entry.captureSet = /* @__PURE__ */ new Set();
164
- entry.captureSet.add(handler);
165
- } else {
166
- if (!entry.bubbleSet) entry.bubbleSet = /* @__PURE__ */ new Set();
167
- entry.bubbleSet.add(handler);
83
+ const setKey = capture ? "captureSet" : "bubbleSet";
84
+ const set = entry[setKey] ??= /* @__PURE__ */ new Set();
85
+ if (!set.has(handler)) {
86
+ set.add(handler);
87
+ element.addEventListener(eventType, handler, capture);
168
88
  }
169
89
  } else {
170
- if (capture) {
171
- entry.capture = handler;
172
- } else {
173
- entry.bubble = handler;
174
- }
90
+ setSlot(element, eventType, phase, entry, handler);
175
91
  }
176
92
  };
177
93
  const isEntryEmpty = (entry) => !entry.capture && !entry.bubble && (!entry.captureSet || entry.captureSet.size === 0) && (!entry.bubbleSet || entry.bubbleSet.size === 0);
@@ -181,34 +97,38 @@ const removeDelegatedEvent = (target, eventType, handler, _options = {}) => {
181
97
  const entry = byEvent.get(eventType);
182
98
  if (!entry) return;
183
99
  if (handler) {
184
- if (entry.captureSet) {
185
- entry.captureSet.delete(handler);
186
- }
187
- if (entry.bubbleSet) {
188
- entry.bubbleSet.delete(handler);
189
- }
190
- if (entry.capture === handler) {
191
- entry.capture = void 0;
192
- }
193
- if (entry.bubble === handler) {
194
- entry.bubble = void 0;
100
+ for (const phase of ["capture", "bubble"]) {
101
+ const set = phase === "capture" ? entry.captureSet : entry.bubbleSet;
102
+ if (set?.delete(handler))
103
+ target.removeEventListener(eventType, handler, phase === "capture");
104
+ if (entry[phase] === handler)
105
+ setSlot(target, eventType, phase, entry, void 0);
195
106
  }
196
- target.removeEventListener(eventType, handler, true);
197
- target.removeEventListener(eventType, handler, false);
198
107
  } else {
199
- entry.capture = void 0;
200
- entry.bubble = void 0;
201
- entry.captureSet = void 0;
202
- entry.bubbleSet = void 0;
108
+ clearPhase(target, eventType, entry, "capture");
109
+ clearPhase(target, eventType, entry, "bubble");
203
110
  }
204
111
  if (isEntryEmpty(entry)) {
205
112
  byEvent.delete(eventType);
206
113
  }
207
114
  };
115
+ const clearHooks = /* @__PURE__ */ new Set();
116
+ const onClearDelegatedEvents = (hook) => {
117
+ clearHooks.add(hook);
118
+ return () => {
119
+ clearHooks.delete(hook);
120
+ };
121
+ };
208
122
  const clearDelegatedEvents = (target) => {
209
123
  const byEvent = elementHandlerMap.get(target);
210
- if (!byEvent) return;
211
- byEvent.clear();
124
+ if (byEvent) {
125
+ for (const [eventType, entry] of byEvent) {
126
+ clearPhase(target, eventType, entry, "capture");
127
+ clearPhase(target, eventType, entry, "bubble");
128
+ }
129
+ byEvent.clear();
130
+ }
131
+ for (const hook of clearHooks) hook(target);
212
132
  };
213
133
  const clearDelegatedEventsDeep = (root) => {
214
134
  clearDelegatedEvents(root);
@@ -246,16 +166,109 @@ const removeDelegatedEventByKey = (element, eventType, phase) => {
246
166
  if (!byEvent) return;
247
167
  const entry = byEvent.get(eventType);
248
168
  if (!entry) return;
249
- if (phase === "capture") {
250
- entry.capture = void 0;
251
- entry.captureSet = void 0;
252
- } else {
253
- entry.bubble = void 0;
254
- entry.bubbleSet = void 0;
255
- }
169
+ clearPhase(element, eventType, entry, phase);
256
170
  if (isEntryEmpty(entry)) byEvent.delete(eventType);
257
171
  };
258
172
 
173
+ const FROM_DOM_MARKER = Symbol("defuss-morph.from-dom");
174
+ const COMMENT_TYPE = "#comment";
175
+ const isCommentVNode = (value) => !!value && typeof value === "object" && value.type === COMMENT_TYPE;
176
+ const HTML_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set([
177
+ "allowfullscreen",
178
+ "async",
179
+ "autofocus",
180
+ "autoplay",
181
+ "checked",
182
+ "controls",
183
+ "default",
184
+ "defer",
185
+ "disabled",
186
+ "formnovalidate",
187
+ "hidden",
188
+ "inert",
189
+ "ismap",
190
+ "itemscope",
191
+ "loop",
192
+ "multiple",
193
+ "muted",
194
+ "nomodule",
195
+ "novalidate",
196
+ "open",
197
+ "playsinline",
198
+ "readonly",
199
+ "required",
200
+ "reversed",
201
+ "selected"
202
+ ]);
203
+ const domAttributeToVNodeValue = (attr) => HTML_BOOLEAN_ATTRIBUTES.has(attr.name.toLowerCase()) ? true : attr.value;
204
+ function parseDOM(input, type, Parser) {
205
+ return new Parser().parseFromString(input, type);
206
+ }
207
+ function isSVG(input, Parser) {
208
+ const doc = parseDOM(input, "image/svg+xml", Parser);
209
+ if (!doc.documentElement) return false;
210
+ return doc.documentElement.nodeName.toLowerCase() === "svg";
211
+ }
212
+ function isHTML(input, Parser) {
213
+ const doc = parseDOM(input, "text/html", Parser);
214
+ return doc.documentElement.querySelectorAll("*").length > 2;
215
+ }
216
+ const isMarkup = (input, Parser) => input.indexOf("<") > -1 && input.indexOf(">") > -1 && (isHTML(input, Parser) || isSVG(input, Parser));
217
+ function renderMarkup(markup, Parser, doc) {
218
+ const parsed = doc ? doc : parseDOM(markup, getMimeType(markup, Parser), Parser);
219
+ if (parsed.body) return Array.from(parsed.body.childNodes);
220
+ return parsed.documentElement ? [parsed.documentElement] : [];
221
+ }
222
+ function getMimeType(input, Parser) {
223
+ if (isSVG(input, Parser)) {
224
+ return "image/svg+xml";
225
+ }
226
+ return "text/html";
227
+ }
228
+ function domNodeToVNode(node) {
229
+ if (node.nodeType === 3) {
230
+ return node.textContent || "";
231
+ }
232
+ if (node.nodeType === 1) {
233
+ const element = node;
234
+ const attributes = {};
235
+ for (let i = 0; i < element.attributes.length; i++) {
236
+ const attr = element.attributes[i];
237
+ attributes[attr.name] = domAttributeToVNodeValue(attr);
238
+ }
239
+ const children = [];
240
+ for (let i = 0; i < element.childNodes.length; i++) {
241
+ const childVNode = domNodeToVNode(element.childNodes[i]);
242
+ children.push(childVNode);
243
+ }
244
+ return {
245
+ type: element.tagName.toLowerCase(),
246
+ attributes: { ...attributes, [FROM_DOM_MARKER]: true },
247
+ children
248
+ };
249
+ }
250
+ if (node.nodeType === 8) {
251
+ return { type: COMMENT_TYPE, value: node.nodeValue ?? "" };
252
+ }
253
+ return "";
254
+ }
255
+ const DOCUMENT_START = /^\s*(?:<!--[\s\S]*?-->\s*)*<(?:!doctype|html|head|body)[\s>/]/i;
256
+ function htmlStringToVNodes(html, Parser) {
257
+ const parser = new Parser();
258
+ const doc = parser.parseFromString(
259
+ DOCUMENT_START.test(html) ? html : `<body>${html}`,
260
+ "text/html"
261
+ );
262
+ const vNodes = [];
263
+ for (let i = 0; i < doc.body.childNodes.length; i++) {
264
+ const vnode = domNodeToVNode(doc.body.childNodes[i]);
265
+ if (vnode !== "") {
266
+ vNodes.push(vnode);
267
+ }
268
+ }
269
+ return vNodes;
270
+ }
271
+
259
272
  const CLASS_ATTRIBUTE_NAME = "class";
260
273
  const XLINK_ATTRIBUTE_NAME = "xlink";
261
274
  const XMLNS_ATTRIBUTE_NAME = "xmlns";
@@ -329,6 +342,11 @@ const getRenderer = (document) => {
329
342
  },
330
343
  createElement: (virtualNode, parentDomElement) => {
331
344
  let newEl;
345
+ if (isCommentVNode(virtualNode)) {
346
+ const comment = document.createComment(virtualNode.value ?? "");
347
+ parentDomElement?.appendChild(comment);
348
+ return comment;
349
+ }
332
350
  try {
333
351
  if (typeof virtualNode === "function" && virtualNode.constructor.name === "AsyncFunction") {
334
352
  newEl = document.createElement("div");
@@ -519,96 +537,6 @@ const getRenderer = (document) => {
519
537
  return renderer;
520
538
  };
521
539
 
522
- const FROM_DOM_MARKER = Symbol("defuss-morph.from-dom");
523
- const HTML_BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set([
524
- "allowfullscreen",
525
- "async",
526
- "autofocus",
527
- "autoplay",
528
- "checked",
529
- "controls",
530
- "default",
531
- "defer",
532
- "disabled",
533
- "formnovalidate",
534
- "hidden",
535
- "inert",
536
- "ismap",
537
- "itemscope",
538
- "loop",
539
- "multiple",
540
- "muted",
541
- "nomodule",
542
- "novalidate",
543
- "open",
544
- "playsinline",
545
- "readonly",
546
- "required",
547
- "reversed",
548
- "selected"
549
- ]);
550
- const domAttributeToVNodeValue = (attr) => HTML_BOOLEAN_ATTRIBUTES.has(attr.name.toLowerCase()) ? true : attr.value;
551
- function parseDOM(input, type, Parser) {
552
- return new Parser().parseFromString(input, type);
553
- }
554
- function isSVG(input, Parser) {
555
- const doc = parseDOM(input, "image/svg+xml", Parser);
556
- if (!doc.documentElement) return false;
557
- return doc.documentElement.nodeName.toLowerCase() === "svg";
558
- }
559
- function isHTML(input, Parser) {
560
- const doc = parseDOM(input, "text/html", Parser);
561
- return doc.documentElement.querySelectorAll("*").length > 2;
562
- }
563
- const isMarkup = (input, Parser) => input.indexOf("<") > -1 && input.indexOf(">") > -1 && (isHTML(input, Parser) || isSVG(input, Parser));
564
- function renderMarkup(markup, Parser, doc) {
565
- const parsed = doc ? doc : parseDOM(markup, getMimeType(markup, Parser), Parser);
566
- if (parsed.body) return Array.from(parsed.body.childNodes);
567
- return parsed.documentElement ? [parsed.documentElement] : [];
568
- }
569
- function getMimeType(input, Parser) {
570
- if (isSVG(input, Parser)) {
571
- return "image/svg+xml";
572
- }
573
- return "text/html";
574
- }
575
- function domNodeToVNode(node) {
576
- if (node.nodeType === 3) {
577
- return node.textContent || "";
578
- }
579
- if (node.nodeType === 1) {
580
- const element = node;
581
- const attributes = {};
582
- for (let i = 0; i < element.attributes.length; i++) {
583
- const attr = element.attributes[i];
584
- attributes[attr.name] = domAttributeToVNodeValue(attr);
585
- }
586
- const children = [];
587
- for (let i = 0; i < element.childNodes.length; i++) {
588
- const childVNode = domNodeToVNode(element.childNodes[i]);
589
- children.push(childVNode);
590
- }
591
- return {
592
- type: element.tagName.toLowerCase(),
593
- attributes: { ...attributes, [FROM_DOM_MARKER]: true },
594
- children
595
- };
596
- }
597
- return "";
598
- }
599
- function htmlStringToVNodes(html, Parser) {
600
- const parser = new Parser();
601
- const doc = parser.parseFromString(html, "text/html");
602
- const vNodes = [];
603
- for (let i = 0; i < doc.body.childNodes.length; i++) {
604
- const vnode = domNodeToVNode(doc.body.childNodes[i]);
605
- if (vnode !== "") {
606
- vNodes.push(vnode);
607
- }
608
- }
609
- return vNodes;
610
- }
611
-
612
540
  const areDomNodesEqual = (oldNode, newNode) => {
613
541
  if (oldNode === newNode) return true;
614
542
  if (oldNode.nodeType !== newNode.nodeType) return false;
@@ -625,8 +553,8 @@ const areDomNodesEqual = (oldNode, newNode) => {
625
553
  if (oldAttr.value !== newAttrValue) return false;
626
554
  }
627
555
  }
628
- if (oldNode.nodeType === 3) {
629
- if (oldNode.textContent !== newNode.textContent) return false;
556
+ if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
557
+ if (oldNode.nodeValue !== newNode.nodeValue) return false;
630
558
  }
631
559
  return true;
632
560
  };
@@ -713,6 +641,7 @@ function areNodeAndChildMatching(domNode, child) {
713
641
  if (typeof child === "string" || typeof child === "number" || typeof child === "boolean") {
714
642
  return domNode.nodeType === 3;
715
643
  }
644
+ if (isCommentVNode(child)) return domNode.nodeType === 8;
716
645
  if (child && typeof child === "object") {
717
646
  if (domNode.nodeType !== 1) return false;
718
647
  const el = domNode;
@@ -798,6 +727,11 @@ function patchElementInPlace(el, vnode, globals, mergeAttributes = false) {
798
727
  return;
799
728
  morphDomDirect(el, vnode.children ?? [], globals);
800
729
  }
730
+ function replaceNode(old, next) {
731
+ if (old.nodeType === 1)
732
+ clearDelegatedEventsDeep(old);
733
+ old.parentNode?.replaceChild(next, old);
734
+ }
801
735
  function morphNode(domNode, child, globals, mergeAttributes = false) {
802
736
  if (typeof child === "string" || typeof child === "number" || typeof child === "boolean") {
803
737
  const text = String(child);
@@ -806,7 +740,17 @@ function morphNode(domNode, child, globals, mergeAttributes = false) {
806
740
  return domNode;
807
741
  }
808
742
  const next = globals.window.document.createTextNode(text);
809
- domNode.parentNode?.replaceChild(next, domNode);
743
+ replaceNode(domNode, next);
744
+ return next;
745
+ }
746
+ if (isCommentVNode(child)) {
747
+ const data = child.value ?? "";
748
+ if (domNode.nodeType === 8) {
749
+ if (domNode.nodeValue !== data) domNode.nodeValue = data;
750
+ return domNode;
751
+ }
752
+ const next = globals.window.document.createComment(data);
753
+ replaceNode(domNode, next);
810
754
  return next;
811
755
  }
812
756
  if (child && typeof child === "object") {
@@ -827,13 +771,15 @@ function morphNode(domNode, child, globals, mergeAttributes = false) {
827
771
  const created = createDomFromChild(child, globals);
828
772
  const first = Array.isArray(created) ? created[0] : created;
829
773
  if (!first) return null;
830
- el.parentNode?.replaceChild(first, el);
774
+ replaceNode(el, first);
831
775
  handleLifecycleEventsForOnMount(first);
832
776
  return first;
833
777
  }
834
778
  patchElementInPlace(el, child, globals, mergeAttributes);
835
779
  return el;
836
780
  }
781
+ if (domNode.nodeType === 1)
782
+ clearDelegatedEventsDeep(domNode);
837
783
  domNode.parentNode?.removeChild(domNode);
838
784
  return null;
839
785
  }
@@ -888,6 +834,7 @@ function morphDiff(targetRoot, patchItems, globals) {
888
834
  }
889
835
  for (const item of patchItems) {
890
836
  if (typeof item === "string" && item.trim() === "") continue;
837
+ if (isCommentVNode(item)) continue;
891
838
  const key = getVNodeMatchKey(item);
892
839
  if (!key) {
893
840
  throw new Error(
@@ -1205,6 +1152,7 @@ const morph = (el, newContent, options = {}) => {
1205
1152
 
1206
1153
  exports.CAPTURE_ONLY_EVENTS = CAPTURE_ONLY_EVENTS;
1207
1154
  exports.CLASS_ATTRIBUTE_NAME = CLASS_ATTRIBUTE_NAME;
1155
+ exports.COMMENT_TYPE = COMMENT_TYPE;
1208
1156
  exports.DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE = DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE;
1209
1157
  exports.DEFAULT_TRANSITION_CONFIG = DEFAULT_TRANSITION_CONFIG;
1210
1158
  exports.FROM_DOM_MARKER = FROM_DOM_MARKER;
@@ -1223,12 +1171,14 @@ exports.getRenderer = getRenderer;
1223
1171
  exports.getTransitionStyles = getTransitionStyles;
1224
1172
  exports.handleLifecycleEventsForOnMount = handleLifecycleEventsForOnMount;
1225
1173
  exports.htmlStringToVNodes = htmlStringToVNodes;
1174
+ exports.isCommentVNode = isCommentVNode;
1226
1175
  exports.isHTML = isHTML;
1227
1176
  exports.isMarkup = isMarkup;
1228
1177
  exports.isSVG = isSVG;
1229
1178
  exports.morph = morph;
1230
1179
  exports.nsMap = nsMap;
1231
1180
  exports.observeUnmount = observeUnmount;
1181
+ exports.onClearDelegatedEvents = onClearDelegatedEvents;
1232
1182
  exports.parseDOM = parseDOM;
1233
1183
  exports.parseEventPropName = parseEventPropName;
1234
1184
  exports.performTransition = performTransition;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,11 @@ interface VNode<A = VNodeAttributes> {
28
28
  type?: VNodeType;
29
29
  attributes?: A;
30
30
  children?: VNodeChildren;
31
+ /**
32
+ * Comment data of a `{ type: "#comment" }` VNode (see `COMMENT_TYPE`).
33
+ * VERIFIED: optional, so defuss core's VNode (no `value`) stays assignable.
34
+ */
35
+ value?: string;
31
36
  sourceInfo?: unknown;
32
37
  /** Original props passed to a function component (set by jsx runtime for SSG hydration). */
33
38
  componentProps?: Record<string, any>;
@@ -82,11 +87,23 @@ interface ParsedEventProp {
82
87
  eventType: string;
83
88
  capture: boolean;
84
89
  }
85
- /** non-bubbling events best handled via capture */
90
+ /**
91
+ * Events whose handlers are forced into the capture phase, so an ancestor's
92
+ * handler also sees descendants' events (container onFocus/onBlur). Every
93
+ * other non-bubbling event reaches only its target's handlers, natively.
94
+ */
86
95
  declare const CAPTURE_ONLY_EVENTS: Set<string>;
87
96
  declare const parseEventPropName: (propName: string) => ParsedEventProp | null;
88
97
  declare const registerDelegatedEvent: (element: HTMLElement, eventType: string, handler: EventListener, options?: DelegatedEventOptions) => void;
89
98
  declare const removeDelegatedEvent: (target: EventTarget, eventType: string, handler?: EventListener, _options?: DelegatedEventOptions) => void;
99
+ /**
100
+ * Why: listeners a facade attaches natively (defuss-query's on()) live
101
+ * outside this registry, yet must go whenever morph clears an element
102
+ * (removal, replacement, clearDelegatedEventsDeep). A hook keeps morph
103
+ * ignorant of who owns them, instead of morph also tracking native listeners.
104
+ * Registering the same hook twice is a no-op; returns an unregister function.
105
+ */
106
+ declare const onClearDelegatedEvents: (hook: (target: EventTarget) => void) => (() => void);
90
107
  declare const clearDelegatedEvents: (target: EventTarget) => void;
91
108
  /**
92
109
  * Clear delegated events for an element and all its descendants.
@@ -130,7 +147,7 @@ declare const getRenderer: (document: Document) => DomAbstractionImpl;
130
147
  * 1. Checks for reference equality.
131
148
  * 2. Compares node types.
132
149
  * 3. For Element nodes, compares tag names and attributes.
133
- * 4. For Text nodes, compares text content.
150
+ * 4. For Text and Comment nodes, compares their data.
134
151
  */
135
152
  declare const areDomNodesEqual: (oldNode: Node, newNode: Node) => boolean;
136
153
  /********************************************************
@@ -182,6 +199,17 @@ declare function replaceDomWithVdom(parentElement: Element, newVDOM: RenderInput
182
199
  * (same rationale as uncontrolled form-state preservation).
183
200
  */
184
201
  declare const FROM_DOM_MARKER: unique symbol;
202
+ /**
203
+ * VNode type of an HTML comment: `{ type: "#comment", value }`.
204
+ * Why "#comment": it is the DOM's own nodeName for comments and can never
205
+ * collide with a tag name ("#" is invalid there). The data lives in `value`,
206
+ * not `children`, so generic tree walkers never mistake it for content.
207
+ */
208
+ declare const COMMENT_TYPE = "#comment";
209
+ type CommentVNode = VNode & {
210
+ type: typeof COMMENT_TYPE;
211
+ };
212
+ declare const isCommentVNode: (value: unknown) => value is CommentVNode;
185
213
  declare function parseDOM(input: string, type: DOMParserSupportedType, Parser: typeof DOMParser): Document;
186
214
  declare function isSVG(input: string, Parser: typeof DOMParser): boolean;
187
215
  declare function isHTML(input: string, Parser: typeof DOMParser): boolean;
@@ -229,5 +257,5 @@ interface MorphOptions {
229
257
  }
230
258
  declare const morph: (el: Element, newContent: RenderInput, options?: MorphOptions) => void | Promise<void>;
231
259
 
232
- export { CAPTURE_ONLY_EVENTS, CLASS_ATTRIBUTE_NAME, DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE, DEFAULT_TRANSITION_CONFIG, FROM_DOM_MARKER, REF_ATTRIBUTE_NAME, XLINK_ATTRIBUTE_NAME, XMLNS_ATTRIBUTE_NAME, applyStyles, areDomNodesEqual, clearDelegatedEvents, clearDelegatedEventsDeep, domNodeToVNode, getMimeType, getRegisteredEventKeys, getRegisteredEventTypes, getRenderer, getTransitionStyles, handleLifecycleEventsForOnMount, htmlStringToVNodes, isHTML, isMarkup, isSVG, morph, nsMap, observeUnmount, parseDOM, parseEventPropName, performTransition, queueCallback, registerDelegatedEvent, removeDelegatedEvent, removeDelegatedEventByKey, renderMarkup, replaceDomWithVdom, resolveGlobals, updateDomWithVdom };
233
- export type { DefussKey, DelegatedEventOptions, DelegatedPhase, DomAbstractionImpl, Globals, MorphMode, MorphOptions, MountHandler, ParsedEventProp, RefLike, RenderInput, TransitionConfig, TransitionStyles, TransitionType, TransitionsEasing, UnmountHandler, VNode, VNodeAttributes, VNodeChild, VNodeChildren, VNodeType, ValidChild };
260
+ export { CAPTURE_ONLY_EVENTS, CLASS_ATTRIBUTE_NAME, COMMENT_TYPE, DANGEROUSLY_SET_INNER_HTML_ATTRIBUTE, DEFAULT_TRANSITION_CONFIG, FROM_DOM_MARKER, REF_ATTRIBUTE_NAME, XLINK_ATTRIBUTE_NAME, XMLNS_ATTRIBUTE_NAME, applyStyles, areDomNodesEqual, clearDelegatedEvents, clearDelegatedEventsDeep, domNodeToVNode, getMimeType, getRegisteredEventKeys, getRegisteredEventTypes, getRenderer, getTransitionStyles, handleLifecycleEventsForOnMount, htmlStringToVNodes, isCommentVNode, isHTML, isMarkup, isSVG, morph, nsMap, observeUnmount, onClearDelegatedEvents, parseDOM, parseEventPropName, performTransition, queueCallback, registerDelegatedEvent, removeDelegatedEvent, removeDelegatedEventByKey, renderMarkup, replaceDomWithVdom, resolveGlobals, updateDomWithVdom };
261
+ export type { CommentVNode, DefussKey, DelegatedEventOptions, DelegatedPhase, DomAbstractionImpl, Globals, MorphMode, MorphOptions, MountHandler, ParsedEventProp, RefLike, RenderInput, TransitionConfig, TransitionStyles, TransitionType, TransitionsEasing, UnmountHandler, VNode, VNodeAttributes, VNodeChild, VNodeChildren, VNodeType, ValidChild };