@b9g/crank 0.7.4 → 0.7.6

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/dom.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var crank = require('./crank.cjs');
4
4
  var _css = require('./_css.cjs');
5
+ var _svg = require('./_svg.cjs');
5
6
 
6
7
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
7
8
  const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
@@ -60,19 +61,325 @@ function emitHydrationWarning(propName, quietProps, expectedValue, actualValue,
60
61
  }
61
62
  }
62
63
  }
64
+ function patchProp(element, name, value, oldValue, props, isSVG, isMathML, copyProps, quietProps, isHydrating) {
65
+ if (copyProps != null && copyProps.has(name)) {
66
+ return;
67
+ }
68
+ // handle prop:name or attr:name properties
69
+ const colonIndex = name.indexOf(":");
70
+ if (colonIndex !== -1) {
71
+ const [ns, name1] = [name.slice(0, colonIndex), name.slice(colonIndex + 1)];
72
+ switch (ns) {
73
+ case "prop":
74
+ element[name1] = value;
75
+ return;
76
+ case "attr":
77
+ if (value == null || value === false) {
78
+ if (isHydrating && element.hasAttribute(name1)) {
79
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
80
+ }
81
+ element.removeAttribute(name1);
82
+ return;
83
+ }
84
+ else if (value === true) {
85
+ if (isHydrating && !element.hasAttribute(name1)) {
86
+ emitHydrationWarning(name, quietProps, value, null, element);
87
+ }
88
+ element.setAttribute(name1, "");
89
+ return;
90
+ }
91
+ if (typeof value !== "string") {
92
+ value = String(value);
93
+ }
94
+ if (isHydrating && element.getAttribute(name1) !== value) {
95
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
96
+ }
97
+ element.setAttribute(name1, value);
98
+ return;
99
+ }
100
+ }
101
+ switch (name) {
102
+ // TODO: fix hydration warnings for the style prop
103
+ case "style": {
104
+ const style = element.style;
105
+ if (value == null || value === false) {
106
+ if (isHydrating && style.cssText !== "") {
107
+ emitHydrationWarning(name, quietProps, value, style.cssText, element);
108
+ }
109
+ element.removeAttribute("style");
110
+ }
111
+ else if (value === true) {
112
+ if (isHydrating && style.cssText !== "") {
113
+ emitHydrationWarning(name, quietProps, "", style.cssText, element);
114
+ }
115
+ element.setAttribute("style", "");
116
+ }
117
+ else if (typeof value === "string") {
118
+ if (style.cssText !== value) {
119
+ // TODO: Fix hydration warnings for styles
120
+ //if (isHydrating) {
121
+ // emitHydrationWarning(
122
+ // name,
123
+ // quietProps,
124
+ // value,
125
+ // style.cssText,
126
+ // element,
127
+ // );
128
+ //}
129
+ style.cssText = value;
130
+ }
131
+ }
132
+ else {
133
+ if (typeof oldValue === "string") {
134
+ // if the old value was a string, we need to clear the style
135
+ // TODO: only clear the styles enumerated in the old value
136
+ style.cssText = "";
137
+ }
138
+ // First pass: remove styles present in oldValue but not in value
139
+ if (oldValue) {
140
+ for (const styleName in oldValue) {
141
+ if (value && styleName in value)
142
+ continue;
143
+ const cssName = _css.camelToKebabCase(styleName);
144
+ if (isHydrating && style.getPropertyValue(cssName) !== "") {
145
+ emitHydrationWarning(name, quietProps, null, style.getPropertyValue(cssName), element, `style.${styleName}`);
146
+ }
147
+ style.removeProperty(cssName);
148
+ }
149
+ }
150
+ // Second pass: apply all styles from value
151
+ if (value) {
152
+ for (const styleName in value) {
153
+ const cssName = _css.camelToKebabCase(styleName);
154
+ const styleValue = value[styleName];
155
+ if (styleValue == null) {
156
+ if (isHydrating && style.getPropertyValue(cssName) !== "") {
157
+ emitHydrationWarning(name, quietProps, null, style.getPropertyValue(cssName), element, `style.${styleName}`);
158
+ }
159
+ style.removeProperty(cssName);
160
+ }
161
+ else {
162
+ const formattedValue = _css.formatStyleValue(cssName, styleValue);
163
+ if (style.getPropertyValue(cssName) !== formattedValue) {
164
+ // TODO: hydration warnings for style props
165
+ //if (isHydrating) {
166
+ // emitHydrationWarning(
167
+ // name,
168
+ // quietProps,
169
+ // formattedValue,
170
+ // style.getPropertyValue(cssName),
171
+ // element,
172
+ // `style.${styleName}`,
173
+ // );
174
+ //}
175
+ style.setProperty(cssName, formattedValue);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ break;
182
+ }
183
+ case "class":
184
+ case "className":
185
+ if (name === "className" && "class" in props)
186
+ break;
187
+ if (value === true) {
188
+ if (isHydrating && element.getAttribute("class") !== "") {
189
+ emitHydrationWarning(name, quietProps, "", element.getAttribute("class"), element);
190
+ }
191
+ element.setAttribute("class", "");
192
+ }
193
+ else if (value == null) {
194
+ if (isHydrating && element.hasAttribute("class")) {
195
+ emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
196
+ }
197
+ element.removeAttribute("class");
198
+ }
199
+ else if (typeof value === "object") {
200
+ // class={{"included-class": true, "excluded-class": false}} syntax
201
+ if (typeof oldValue === "string") {
202
+ // if the old value was a string, we need to clear all classes
203
+ element.setAttribute("class", "");
204
+ }
205
+ let shouldIssueWarning = false;
206
+ const hydratingClasses = isHydrating
207
+ ? new Set(Array.from(element.classList))
208
+ : undefined;
209
+ const hydratingClassName = isHydrating
210
+ ? element.getAttribute("class")
211
+ : undefined;
212
+ // Two passes: removes first, then adds. This ensures that
213
+ // overlapping classes in different keys are handled correctly.
214
+ // e.g. {"a b": false, "b c": true} should result in "b c"
215
+ // Remove pass: iterate oldValue for classes to remove
216
+ if (oldValue) {
217
+ for (const classNames in oldValue) {
218
+ if (value && value[classNames])
219
+ continue;
220
+ const classes = classNames.split(/\s+/).filter(Boolean);
221
+ element.classList.remove(...classes);
222
+ }
223
+ }
224
+ // Add pass: iterate value for classes to add
225
+ if (value) {
226
+ for (const classNames in value) {
227
+ if (!value[classNames])
228
+ continue;
229
+ const classes = classNames.split(/\s+/).filter(Boolean);
230
+ element.classList.add(...classes);
231
+ for (const className of classes) {
232
+ if (hydratingClasses && hydratingClasses.has(className)) {
233
+ hydratingClasses.delete(className);
234
+ }
235
+ else if (isHydrating) {
236
+ shouldIssueWarning = true;
237
+ }
238
+ }
239
+ }
240
+ }
241
+ if (shouldIssueWarning ||
242
+ (hydratingClasses && hydratingClasses.size > 0)) {
243
+ emitHydrationWarning(name, quietProps, Object.keys(value)
244
+ .filter((k) => value[k])
245
+ .join(" "), hydratingClassName || "", element);
246
+ }
247
+ }
248
+ else if (!isSVG && !isMathML) {
249
+ if (element.className !== value) {
250
+ if (isHydrating) {
251
+ emitHydrationWarning(name, quietProps, value, element.className, element);
252
+ }
253
+ element.className = value;
254
+ }
255
+ }
256
+ else if (element.getAttribute("class") !== value) {
257
+ if (isHydrating) {
258
+ emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
259
+ }
260
+ element.setAttribute("class", value);
261
+ }
262
+ break;
263
+ case "innerHTML":
264
+ if (value !== oldValue) {
265
+ if (isHydrating) {
266
+ emitHydrationWarning(name, quietProps, value, element.innerHTML, element);
267
+ }
268
+ element.innerHTML = value;
269
+ }
270
+ break;
271
+ case "dangerouslySetInnerHTML": {
272
+ const htmlValue = value && typeof value === "object" && "__html" in value
273
+ ? (value.__html ?? "")
274
+ : "";
275
+ const oldHtmlValue = oldValue && typeof oldValue === "object" && "__html" in oldValue
276
+ ? (oldValue.__html ?? "")
277
+ : "";
278
+ if (htmlValue !== oldHtmlValue) {
279
+ element.innerHTML = htmlValue;
280
+ }
281
+ break;
282
+ }
283
+ case "htmlFor":
284
+ if ("for" in props)
285
+ break;
286
+ if (value == null || value === false) {
287
+ element.removeAttribute("for");
288
+ }
289
+ else {
290
+ element.setAttribute("for", String(value === true ? "" : value));
291
+ }
292
+ break;
293
+ default: {
294
+ if (name[0] === "o" &&
295
+ name[1] === "n" &&
296
+ name[2] === name[2].toUpperCase() &&
297
+ typeof value === "function") {
298
+ // Support React-style event names (onClick, onChange, etc.)
299
+ name = name.toLowerCase();
300
+ }
301
+ // Support React-style SVG attribute names (strokeWidth, etc.)
302
+ if (isSVG && name in _svg.REACT_SVG_PROPS) {
303
+ name = _svg.REACT_SVG_PROPS[name];
304
+ }
305
+ // try to set the property directly
306
+ if (name in element &&
307
+ // boolean properties will coerce strings, but sometimes they map to
308
+ // enumerated attributes, where truthy strings ("false", "no") map to
309
+ // falsy properties, so we force using setAttribute.
310
+ !(typeof value === "string" &&
311
+ typeof element[name] === "boolean") &&
312
+ isWritableProperty(element, name)) {
313
+ // For URL properties like src and href, the DOM property returns the
314
+ // resolved absolute URL. We need to resolve the prop value the same way
315
+ // to compare correctly.
316
+ let domValue = element[name];
317
+ let propValue = value;
318
+ if ((name === "src" || name === "href") &&
319
+ typeof value === "string" &&
320
+ typeof domValue === "string") {
321
+ try {
322
+ propValue = new URL(value, element.baseURI).href;
323
+ }
324
+ catch {
325
+ // Invalid URL, use original value for comparison
326
+ }
327
+ }
328
+ if (propValue !== domValue || oldValue === undefined) {
329
+ if (isHydrating &&
330
+ typeof element[name] === "string" &&
331
+ element[name] !== value) {
332
+ emitHydrationWarning(name, quietProps, value, element[name], element);
333
+ }
334
+ // if the property is writable, assign it directly
335
+ element[name] = value;
336
+ }
337
+ return;
338
+ }
339
+ if (value === true) {
340
+ value = "";
341
+ }
342
+ else if (value == null || value === false) {
343
+ if (isHydrating && element.hasAttribute(name)) {
344
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
345
+ }
346
+ element.removeAttribute(name);
347
+ return;
348
+ }
349
+ else if (typeof value !== "string") {
350
+ value = String(value);
351
+ }
352
+ if (element.getAttribute(name) !== value) {
353
+ if (isHydrating) {
354
+ emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
355
+ }
356
+ element.setAttribute(name, value);
357
+ }
358
+ }
359
+ }
360
+ }
63
361
  const adapter = {
64
- scope({ scope: xmlns, tag, props, }) {
362
+ scope({ scope: xmlns, tag, props, root, }) {
65
363
  switch (tag) {
66
- case crank.Portal:
67
- // TODO: read the namespace from the portal root element
68
- xmlns = undefined;
364
+ case crank.Portal: {
365
+ const ns = root instanceof Element ? root.namespaceURI : null;
366
+ xmlns =
367
+ ns === SVG_NAMESPACE
368
+ ? SVG_NAMESPACE
369
+ : ns === MATHML_NAMESPACE
370
+ ? MATHML_NAMESPACE
371
+ : undefined;
69
372
  break;
373
+ }
70
374
  case "svg":
71
375
  xmlns = SVG_NAMESPACE;
72
376
  break;
73
377
  case "math":
74
378
  xmlns = MATHML_NAMESPACE;
75
379
  break;
380
+ case "foreignObject":
381
+ xmlns = undefined;
382
+ break;
76
383
  }
77
384
  return props.xmlns || xmlns;
78
385
  },
@@ -86,6 +393,9 @@ const adapter = {
86
393
  else if (tag.toLowerCase() === "math") {
87
394
  xmlns = MATHML_NAMESPACE;
88
395
  }
396
+ else if (tag === "foreignObject") {
397
+ xmlns = SVG_NAMESPACE;
398
+ }
89
399
  const doc = getRootDocument(root);
90
400
  return xmlns ? doc.createElementNS(xmlns, tag) : doc.createElement(tag);
91
401
  },
@@ -109,7 +419,7 @@ const adapter = {
109
419
  }
110
420
  return Array.from(node.childNodes);
111
421
  },
112
- patch({ tagName, node, props, oldProps, scope: xmlns, copyProps, quietProps, isHydrating, }) {
422
+ patch({ tag, tagName, node, props, oldProps, scope: xmlns, copyProps, quietProps, isHydrating, }) {
113
423
  if (node.nodeType !== Node.ELEMENT_NODE) {
114
424
  throw new TypeError(`Cannot patch node: ${String(node)}`);
115
425
  }
@@ -117,268 +427,26 @@ const adapter = {
117
427
  console.error(`Both "class" and "className" set in props for <${tagName}>. Use one or the other.`);
118
428
  }
119
429
  const element = node;
120
- const isSVG = xmlns === SVG_NAMESPACE;
430
+ const isSVG = xmlns === SVG_NAMESPACE || tag === "foreignObject";
121
431
  const isMathML = xmlns === MATHML_NAMESPACE;
122
- for (let name in { ...oldProps, ...props }) {
123
- let value = props[name];
124
- const oldValue = oldProps ? oldProps[name] : undefined;
125
- {
126
- if (copyProps != null && copyProps.has(name)) {
432
+ // First pass: iterate oldProps to handle removals
433
+ if (oldProps) {
434
+ for (let name in oldProps) {
435
+ if (name in props)
127
436
  continue;
128
- }
129
- // handle prop:name or attr:name properties
130
- const colonIndex = name.indexOf(":");
131
- if (colonIndex !== -1) {
132
- const [ns, name1] = [
133
- name.slice(0, colonIndex),
134
- name.slice(colonIndex + 1),
135
- ];
136
- switch (ns) {
137
- case "prop":
138
- node[name1] = value;
139
- continue;
140
- case "attr":
141
- if (value == null || value === false) {
142
- if (isHydrating && element.hasAttribute(name1)) {
143
- emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
144
- }
145
- element.removeAttribute(name1);
146
- }
147
- else if (value === true) {
148
- if (isHydrating && !element.hasAttribute(name1)) {
149
- emitHydrationWarning(name, quietProps, value, null, element);
150
- }
151
- element.setAttribute(name1, "");
152
- }
153
- else if (typeof value !== "string") {
154
- value = String(value);
155
- }
156
- if (isHydrating && element.getAttribute(name1) !== value) {
157
- emitHydrationWarning(name, quietProps, value, element.getAttribute(name1), element);
158
- }
159
- element.setAttribute(name1, String(value));
160
- continue;
161
- }
162
- }
163
- }
164
- switch (name) {
165
- // TODO: fix hydration warnings for the style prop
166
- case "style": {
167
- const style = element.style;
168
- if (value == null || value === false) {
169
- if (isHydrating && style.cssText !== "") {
170
- emitHydrationWarning(name, quietProps, value, style.cssText, element);
171
- }
172
- element.removeAttribute("style");
173
- }
174
- else if (value === true) {
175
- if (isHydrating && style.cssText !== "") {
176
- emitHydrationWarning(name, quietProps, "", style.cssText, element);
177
- }
178
- element.setAttribute("style", "");
179
- }
180
- else if (typeof value === "string") {
181
- if (style.cssText !== value) {
182
- // TODO: Fix hydration warnings for styles
183
- //if (isHydrating) {
184
- // emitHydrationWarning(
185
- // name,
186
- // quietProps,
187
- // value,
188
- // style.cssText,
189
- // element,
190
- // );
191
- //}
192
- style.cssText = value;
193
- }
194
- }
195
- else {
196
- if (typeof oldValue === "string") {
197
- // if the old value was a string, we need to clear the style
198
- // TODO: only clear the styles enumerated in the old value
199
- style.cssText = "";
200
- }
201
- for (const styleName in { ...oldValue, ...value }) {
202
- const cssName = _css.camelToKebabCase(styleName);
203
- const styleValue = value && value[styleName];
204
- if (styleValue == null) {
205
- if (isHydrating && style.getPropertyValue(cssName) !== "") {
206
- emitHydrationWarning(name, quietProps, null, style.getPropertyValue(cssName), element, `style.${styleName}`);
207
- }
208
- style.removeProperty(cssName);
209
- }
210
- else {
211
- const formattedValue = _css.formatStyleValue(cssName, styleValue);
212
- if (style.getPropertyValue(cssName) !== formattedValue) {
213
- // TODO: hydration warnings for style props
214
- //if (isHydrating) {
215
- // emitHydrationWarning(
216
- // name,
217
- // quietProps,
218
- // formattedValue,
219
- // style.getPropertyValue(cssName),
220
- // element,
221
- // `style.${styleName}`,
222
- // );
223
- //}
224
- style.setProperty(cssName, formattedValue);
225
- }
226
- }
227
- }
228
- }
229
- break;
230
- }
231
- case "class":
232
- case "className":
233
- if (value === true) {
234
- if (isHydrating && element.getAttribute("class") !== "") {
235
- emitHydrationWarning(name, quietProps, "", element.getAttribute("class"), element);
236
- }
237
- element.setAttribute("class", "");
238
- }
239
- else if (value == null) {
240
- if (isHydrating && element.hasAttribute("class")) {
241
- emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
242
- }
243
- element.removeAttribute("class");
244
- }
245
- else if (typeof value === "object") {
246
- // class={{"included-class": true, "excluded-class": false}} syntax
247
- if (typeof oldValue === "string") {
248
- // if the old value was a string, we need to clear all classes
249
- element.setAttribute("class", "");
250
- }
251
- let shouldIssueWarning = false;
252
- const hydratingClasses = isHydrating
253
- ? new Set(Array.from(element.classList))
254
- : undefined;
255
- const hydratingClassName = isHydrating
256
- ? element.getAttribute("class")
257
- : undefined;
258
- const allClassNames = { ...oldValue, ...value };
259
- // Two passes: removes first, then adds. This ensures that
260
- // overlapping classes in different keys are handled correctly.
261
- // e.g. {"a b": false, "b c": true} should result in "b c"
262
- for (const classNames in allClassNames) {
263
- if (!(value && value[classNames])) {
264
- const classes = classNames.split(/\s+/).filter(Boolean);
265
- element.classList.remove(...classes);
266
- }
267
- }
268
- for (const classNames in allClassNames) {
269
- if (value && value[classNames]) {
270
- const classes = classNames.split(/\s+/).filter(Boolean);
271
- element.classList.add(...classes);
272
- for (const className of classes) {
273
- if (hydratingClasses && hydratingClasses.has(className)) {
274
- hydratingClasses.delete(className);
275
- }
276
- else if (isHydrating) {
277
- shouldIssueWarning = true;
278
- }
279
- }
280
- }
281
- }
282
- if (shouldIssueWarning ||
283
- (hydratingClasses && hydratingClasses.size > 0)) {
284
- emitHydrationWarning(name, quietProps, Object.keys(value)
285
- .filter((k) => value[k])
286
- .join(" "), hydratingClassName || "", element);
287
- }
288
- }
289
- else if (!isSVG && !isMathML) {
290
- if (element.className !== value) {
291
- if (isHydrating) {
292
- emitHydrationWarning(name, quietProps, value, element.className, element);
293
- }
294
- element.className = value;
295
- }
296
- }
297
- else if (element.getAttribute("class") !== value) {
298
- if (isHydrating) {
299
- emitHydrationWarning(name, quietProps, value, element.getAttribute("class"), element);
300
- }
301
- element.setAttribute("class", value);
302
- }
303
- break;
304
- case "innerHTML":
305
- if (value !== oldValue) {
306
- if (isHydrating) {
307
- emitHydrationWarning(name, quietProps, value, element.innerHTML, element);
308
- }
309
- element.innerHTML = value;
310
- }
311
- break;
312
- default: {
313
- if (name[0] === "o" &&
314
- name[1] === "n" &&
315
- name[2] === name[2].toUpperCase() &&
316
- typeof value === "function") {
317
- // Support React-style event names (onClick, onChange, etc.)
318
- name = name.toLowerCase();
319
- }
320
- // try to set the property directly
321
- if (name in element &&
322
- // boolean properties will coerce strings, but sometimes they map to
323
- // enumerated attributes, where truthy strings ("false", "no") map to
324
- // falsy properties, so we force using setAttribute.
325
- !(typeof value === "string" &&
326
- typeof element[name] === "boolean") &&
327
- isWritableProperty(element, name)) {
328
- // For URL properties like src and href, the DOM property returns the
329
- // resolved absolute URL. We need to resolve the prop value the same way
330
- // to compare correctly.
331
- let domValue = element[name];
332
- let propValue = value;
333
- if ((name === "src" || name === "href") &&
334
- typeof value === "string" &&
335
- typeof domValue === "string") {
336
- try {
337
- propValue = new URL(value, element.baseURI).href;
338
- }
339
- catch {
340
- // Invalid URL, use original value for comparison
341
- }
342
- }
343
- if (propValue !== domValue || oldValue === undefined) {
344
- if (isHydrating &&
345
- typeof element[name] === "string" &&
346
- element[name] !== value) {
347
- emitHydrationWarning(name, quietProps, value, element[name], element);
348
- }
349
- // if the property is writable, assign it directly
350
- element[name] = value;
351
- }
352
- continue;
353
- }
354
- if (value === true) {
355
- value = "";
356
- }
357
- else if (value == null || value === false) {
358
- if (isHydrating && element.hasAttribute(name)) {
359
- emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
360
- }
361
- element.removeAttribute(name);
362
- continue;
363
- }
364
- else if (typeof value !== "string") {
365
- value = String(value);
366
- }
367
- if (element.getAttribute(name) !== value) {
368
- if (isHydrating) {
369
- emitHydrationWarning(name, quietProps, value, element.getAttribute(name), element);
370
- }
371
- element.setAttribute(name, value);
372
- }
373
- }
437
+ patchProp(element, name, undefined, oldProps[name], props, isSVG, isMathML, copyProps, quietProps, isHydrating);
374
438
  }
375
439
  }
440
+ // Second pass: iterate props to handle additions and updates
441
+ for (let name in props) {
442
+ patchProp(element, name, props[name], oldProps ? oldProps[name] : undefined, props, isSVG, isMathML, copyProps, quietProps, isHydrating);
443
+ }
376
444
  },
377
445
  arrange({ tag, node, props, children, }) {
378
446
  if (tag === crank.Portal && (node == null || typeof node.nodeType !== "number")) {
379
447
  throw new TypeError(`<Portal> root is not a node. Received: ${String(node)}`);
380
448
  }
381
- if (!("innerHTML" in props)) {
449
+ if (!("innerHTML" in props) && !("dangerouslySetInnerHTML" in props)) {
382
450
  let oldChild = node.firstChild;
383
451
  for (let i = 0; i < children.length; i++) {
384
452
  const newChild = children[i];