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