@b9g/crank 0.7.0 → 0.7.2

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/umd.js CHANGED
@@ -510,6 +510,7 @@
510
510
  const IsInForAwaitOfLoop = 1 << 14;
511
511
  const NeedsToYield = 1 << 15;
512
512
  const PropsAvailable = 1 << 16;
513
+ const IsSchedulingRefresh = 1 << 17;
513
514
  function getFlag(ret, flag) {
514
515
  return !!(ret.f & flag);
515
516
  }
@@ -863,10 +864,10 @@
863
864
  }
864
865
  }
865
866
  else if (ret) {
867
+ let candidateFound = false;
866
868
  // we do not need to add the retainer to the graveyard if it is the
867
869
  // fallback of another retainer
868
870
  // search for the tag in fallback chain
869
- let candidateFound = false;
870
871
  for (let predecessor = ret, candidate = ret.fallback; candidate; predecessor = candidate, candidate = candidate.fallback) {
871
872
  if (candidate.el.tag === child.tag) {
872
873
  // If we find a retainer in the fallback chain with the same tag,
@@ -1701,6 +1702,9 @@
1701
1702
  });
1702
1703
  }
1703
1704
  }
1705
+ if (getFlag(ctx.ret, IsScheduling)) {
1706
+ setFlag(ctx.ret, IsSchedulingRefresh);
1707
+ }
1704
1708
  let diff;
1705
1709
  const schedulePromises = [];
1706
1710
  try {
@@ -2046,10 +2050,11 @@
2046
2050
  if (getFlag(ctx.ret, IsInForOfLoop) &&
2047
2051
  !getFlag(ctx.ret, NeedsToYield) &&
2048
2052
  !getFlag(ctx.ret, IsUnmounted) &&
2049
- !getFlag(ctx.ret, IsScheduling)) {
2053
+ !getFlag(ctx.ret, IsSchedulingRefresh)) {
2050
2054
  console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
2051
2055
  }
2052
2056
  setFlag(ctx.ret, NeedsToYield, false);
2057
+ setFlag(ctx.ret, IsSchedulingRefresh, false);
2053
2058
  if (iteration.done) {
2054
2059
  setFlag(ctx.ret, IsSyncGen, false);
2055
2060
  ctx.iterator = undefined;
@@ -2095,11 +2100,12 @@
2095
2100
  if (getFlag(ctx.ret, IsInForOfLoop) &&
2096
2101
  !getFlag(ctx.ret, NeedsToYield) &&
2097
2102
  !getFlag(ctx.ret, IsUnmounted) &&
2098
- !getFlag(ctx.ret, IsScheduling)) {
2103
+ !getFlag(ctx.ret, IsSchedulingRefresh)) {
2099
2104
  console.error(`Component <${getTagName(ctx.ret.el.tag)}> yielded/returned more than once in for...of loop`);
2100
2105
  }
2101
2106
  }
2102
2107
  setFlag(ctx.ret, NeedsToYield, false);
2108
+ setFlag(ctx.ret, IsSchedulingRefresh, false);
2103
2109
  if (iteration.done) {
2104
2110
  setFlag(ctx.ret, IsAsyncGen, false);
2105
2111
  ctx.iterator = undefined;
@@ -2331,18 +2337,17 @@
2331
2337
  });
2332
2338
  return getValue(ctx.ret);
2333
2339
  }
2334
- const wasScheduling = getFlag(ctx.ret, IsScheduling);
2335
2340
  const values = commitChildren(ctx.adapter, ctx.host, ctx, ctx.scope, ctx.ret, ctx.index, schedulePromises, hydrationNodes);
2336
2341
  if (getFlag(ctx.ret, IsUnmounted)) {
2337
2342
  return;
2338
2343
  }
2339
2344
  addEventTargetDelegates(ctx.ctx, values);
2340
2345
  // Execute schedule callbacks early to check for async deferral
2341
- const callbacks = scheduleMap.get(ctx);
2346
+ const wasScheduling = getFlag(ctx.ret, IsScheduling);
2342
2347
  let schedulePromises1;
2348
+ const callbacks = scheduleMap.get(ctx);
2343
2349
  if (callbacks) {
2344
2350
  scheduleMap.delete(ctx);
2345
- // TODO: think about error handling for schedule callbacks
2346
2351
  setFlag(ctx.ret, IsScheduling);
2347
2352
  const result = ctx.adapter.read(unwrap(values));
2348
2353
  for (const callback of callbacks) {
@@ -2353,7 +2358,7 @@
2353
2358
  }
2354
2359
  if (schedulePromises1 && !getFlag(ctx.ret, DidCommit)) {
2355
2360
  const scheduleCallbacksP = Promise.all(schedulePromises1).then(() => {
2356
- setFlag(ctx.ret, IsScheduling, false);
2361
+ setFlag(ctx.ret, IsScheduling, wasScheduling);
2357
2362
  propagateComponent(ctx);
2358
2363
  if (ctx.ret.fallback) {
2359
2364
  unmount(ctx.adapter, ctx.host, ctx.parent, ctx.ret.fallback, false);
@@ -2393,6 +2398,37 @@
2393
2398
  // if schedule callbacks call refresh() or async mounting is happening.
2394
2399
  return getValue(ctx.ret, true);
2395
2400
  }
2401
+ /**
2402
+ * Checks if a target retainer is active (contributing) in the host's retainer tree.
2403
+ * Performs a downward traversal from host to find if target is in the active path.
2404
+ */
2405
+ function isRetainerActive(target, host) {
2406
+ const stack = [host];
2407
+ while (stack.length > 0) {
2408
+ const current = stack.pop();
2409
+ if (current === target) {
2410
+ return true;
2411
+ }
2412
+ // Add direct children to stack (skip if this is a host boundary)
2413
+ // Host boundaries are: DOM elements (string tags) or Portal, but NOT Fragment
2414
+ const isHostBoundary = current !== host &&
2415
+ ((typeof current.el.tag === "string" && current.el.tag !== Fragment) ||
2416
+ current.el.tag === Portal);
2417
+ if (current.children && !isHostBoundary) {
2418
+ const children = wrap(current.children);
2419
+ for (const child of children) {
2420
+ if (child) {
2421
+ stack.push(child);
2422
+ }
2423
+ }
2424
+ }
2425
+ // Add fallback chains (only if current retainer is using fallback)
2426
+ if (current.fallback && !getFlag(current, DidDiff)) {
2427
+ stack.push(current.fallback);
2428
+ }
2429
+ }
2430
+ return false;
2431
+ }
2396
2432
  /**
2397
2433
  * Propagates component changes up to ancestors when rendering starts from a
2398
2434
  * component via refresh() or multiple for await...of renders. This handles
@@ -2403,14 +2439,20 @@
2403
2439
  const values = getChildValues(ctx.ret, ctx.index);
2404
2440
  addEventTargetDelegates(ctx.ctx, values, (ctx1) => ctx1[_ContextState].host === ctx.host);
2405
2441
  const host = ctx.host;
2442
+ const initiator = ctx.ret;
2443
+ // Check if initiator is active in the host's tree
2444
+ if (!isRetainerActive(initiator, host)) {
2445
+ return;
2446
+ }
2406
2447
  const props = stripSpecialProps(host.el.props);
2448
+ const hostChildren = getChildValues(host, 0);
2407
2449
  ctx.adapter.arrange({
2408
2450
  tag: host.el.tag,
2409
2451
  tagName: getTagName(host.el.tag),
2410
2452
  node: host.value,
2411
2453
  props,
2412
2454
  oldProps: props,
2413
- children: getChildValues(host, 0),
2455
+ children: hostChildren,
2414
2456
  });
2415
2457
  flush(ctx.adapter, ctx.root, ctx);
2416
2458
  }
@@ -2610,7 +2652,82 @@
2610
2652
  commitComponent(parent, schedulePromises);
2611
2653
  }
2612
2654
 
2655
+ /**
2656
+ * CSS utility functions for style property transformation.
2657
+ *
2658
+ * This module handles camelCase to kebab-case conversion and automatic
2659
+ * px unit conversion for numeric CSS values, making Crank more React-compatible.
2660
+ */
2661
+ /**
2662
+ * Converts camelCase CSS property names to kebab-case.
2663
+ * Handles vendor prefixes correctly (WebkitTransform -> -webkit-transform).
2664
+ */
2665
+ function camelToKebabCase(str) {
2666
+ // Handle vendor prefixes that start with capital letters (WebkitTransform -> -webkit-transform)
2667
+ if (/^[A-Z]/.test(str)) {
2668
+ return `-${str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`).slice(1)}`;
2669
+ }
2670
+ // Handle normal camelCase (fontSize -> font-size)
2671
+ return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
2672
+ }
2673
+ /**
2674
+ * CSS properties that should remain unitless when given numeric values.
2675
+ * Based on React's list of unitless properties.
2676
+ */
2677
+ const UNITLESS_PROPERTIES = new Set([
2678
+ "animation-iteration-count",
2679
+ "aspect-ratio",
2680
+ "border-image-outset",
2681
+ "border-image-slice",
2682
+ "border-image-width",
2683
+ "box-flex",
2684
+ "box-flex-group",
2685
+ "box-ordinal-group",
2686
+ "column-count",
2687
+ "columns",
2688
+ "flex",
2689
+ "flex-grow",
2690
+ "flex-positive",
2691
+ "flex-shrink",
2692
+ "flex-negative",
2693
+ "flex-order",
2694
+ "font-weight",
2695
+ "grid-area",
2696
+ "grid-column",
2697
+ "grid-column-end",
2698
+ "grid-column-span",
2699
+ "grid-column-start",
2700
+ "grid-row",
2701
+ "grid-row-end",
2702
+ "grid-row-span",
2703
+ "grid-row-start",
2704
+ "line-height",
2705
+ "opacity",
2706
+ "order",
2707
+ "orphans",
2708
+ "tab-size",
2709
+ "widows",
2710
+ "z-index",
2711
+ "zoom",
2712
+ ]);
2713
+ /**
2714
+ * Formats CSS property values, automatically adding "px" to numeric values
2715
+ * for properties that are not unitless.
2716
+ */
2717
+ function formatStyleValue(name, value) {
2718
+ if (typeof value === "number") {
2719
+ // If the property should remain unitless, keep the number as-is
2720
+ if (UNITLESS_PROPERTIES.has(name)) {
2721
+ return String(value);
2722
+ }
2723
+ // Otherwise, append "px" for numeric values
2724
+ return `${value}px`;
2725
+ }
2726
+ return String(value);
2727
+ }
2728
+
2613
2729
  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
2730
+ const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
2614
2731
  function isWritableProperty(element, name) {
2615
2732
  // walk up the object's prototype chain to find the owner
2616
2733
  let propOwner = element;
@@ -2660,6 +2777,9 @@
2660
2777
  case "svg":
2661
2778
  xmlns = SVG_NAMESPACE;
2662
2779
  break;
2780
+ case "math":
2781
+ xmlns = MATHML_NAMESPACE;
2782
+ break;
2663
2783
  }
2664
2784
  return props.xmlns || xmlns;
2665
2785
  },
@@ -2670,6 +2790,9 @@
2670
2790
  else if (tag.toLowerCase() === "svg") {
2671
2791
  xmlns = SVG_NAMESPACE;
2672
2792
  }
2793
+ else if (tag.toLowerCase() === "math") {
2794
+ xmlns = MATHML_NAMESPACE;
2795
+ }
2673
2796
  return xmlns
2674
2797
  ? document.createElementNS(xmlns, tag)
2675
2798
  : document.createElement(tag);
@@ -2702,6 +2825,7 @@
2702
2825
  }
2703
2826
  const element = node;
2704
2827
  const isSVG = xmlns === SVG_NAMESPACE;
2828
+ const isMathML = xmlns === MATHML_NAMESPACE;
2705
2829
  for (let name in { ...oldProps, ...props }) {
2706
2830
  let value = props[name];
2707
2831
  const oldValue = oldProps ? oldProps[name] : undefined;
@@ -2782,26 +2906,30 @@
2782
2906
  style.cssText = "";
2783
2907
  }
2784
2908
  for (const styleName in { ...oldValue, ...value }) {
2909
+ const cssName = camelToKebabCase(styleName);
2785
2910
  const styleValue = value && value[styleName];
2786
2911
  if (styleValue == null) {
2787
- if (isHydrating && style.getPropertyValue(styleName) !== "") {
2788
- emitHydrationWarning(name, quietProps, null, style.getPropertyValue(styleName), element, `style.${styleName}`);
2912
+ if (isHydrating && style.getPropertyValue(cssName) !== "") {
2913
+ emitHydrationWarning(name, quietProps, null, style.getPropertyValue(cssName), element, `style.${styleName}`);
2789
2914
  }
2790
- style.removeProperty(styleName);
2915
+ style.removeProperty(cssName);
2791
2916
  }
2792
- else if (style.getPropertyValue(styleName) !== styleValue) {
2793
- // TODO: hydration warnings for style props
2794
- //if (isHydrating) {
2795
- // emitHydrationWarning(
2796
- // name,
2797
- // quietProps,
2798
- // styleValue,
2799
- // style.getPropertyValue(styleName),
2800
- // element,
2801
- // `style.${styleName}`,
2802
- // );
2803
- //}
2804
- style.setProperty(styleName, styleValue);
2917
+ else {
2918
+ const formattedValue = formatStyleValue(cssName, styleValue);
2919
+ if (style.getPropertyValue(cssName) !== formattedValue) {
2920
+ // TODO: hydration warnings for style props
2921
+ //if (isHydrating) {
2922
+ // emitHydrationWarning(
2923
+ // name,
2924
+ // quietProps,
2925
+ // formattedValue,
2926
+ // style.getPropertyValue(cssName),
2927
+ // element,
2928
+ // `style.${styleName}`,
2929
+ // );
2930
+ //}
2931
+ style.setProperty(cssName, formattedValue);
2932
+ }
2805
2933
  }
2806
2934
  }
2807
2935
  }
@@ -2856,7 +2984,7 @@
2856
2984
  .join(" "), hydratingClassName || "", element);
2857
2985
  }
2858
2986
  }
2859
- else if (!isSVG) {
2987
+ else if (!isSVG && !isMathML) {
2860
2988
  if (element.className !== value) {
2861
2989
  if (isHydrating) {
2862
2990
  emitHydrationWarning(name, quietProps, value, element.className, element);
@@ -2997,7 +3125,9 @@
2997
3125
  if (typeof value === "string") {
2998
3126
  const el = xmlns == null
2999
3127
  ? document.createElement("div")
3000
- : document.createElementNS(xmlns, "svg");
3128
+ : xmlns === SVG_NAMESPACE
3129
+ ? document.createElementNS(xmlns, "svg")
3130
+ : document.createElementNS(xmlns, "math");
3001
3131
  el.innerHTML = value;
3002
3132
  nodes = Array.from(el.childNodes);
3003
3133
  }
@@ -3098,7 +3228,9 @@
3098
3228
  const cssStrings = [];
3099
3229
  for (const [name, value] of Object.entries(style)) {
3100
3230
  if (value != null) {
3101
- cssStrings.push(`${name}:${value};`);
3231
+ const cssName = camelToKebabCase(name);
3232
+ const cssValue = formatStyleValue(cssName, value);
3233
+ cssStrings.push(`${cssName}:${cssValue};`);
3102
3234
  }
3103
3235
  }
3104
3236
  return cssStrings.join("");
@@ -3113,7 +3245,7 @@
3113
3245
  if (typeof value === "string") {
3114
3246
  attrs.push(`style="${escape(value)}"`);
3115
3247
  }
3116
- else if (typeof value === "object") {
3248
+ else if (typeof value === "object" && value !== null) {
3117
3249
  attrs.push(`style="${escape(printStyleObject(value))}"`);
3118
3250
  }
3119
3251
  }