sygnal 5.4.0 → 6.0.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.
Files changed (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +9 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +19 -3
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +101 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +1 -1
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
@@ -4,15 +4,24 @@ import {Scope} from './isolate';
4
4
  import {isEqualNamespace} from './utils';
5
5
  import SymbolTree from './SymbolTree';
6
6
 
7
+ // G-356 (D196): the element an event bubbles to and a scope walk goes up to: a moved element's
8
+ // home (`__sygnalHome`), else its parent. G-381: a home inside the element (a cycle) isn't
9
+ // followed, nor more than 98 homes per walk (`c` counts them: two elements naming each other)
10
+ export const upOf = (e: any, c: any): any => {
11
+ const h = e.__sygnalHome;
12
+ return h && !e.contains(h) && (c.$h = (c.$h | 0) + 1) < 99 ? h : e.parentNode;
13
+ };
14
+
7
15
  export class IsolateModule {
8
- private namespaceTree = new SymbolTree<Element, Scope>(x => x.scope);
9
- private namespaceByElement: Map<Element, Array<Scope>>;
16
+ // G-144: a scope can own several root elements (a fragment-rooted component)
17
+ private namespaceTree = new SymbolTree<Set<Element>, Scope>(x => x.scope);
18
+ private namespaceByElement: WeakMap<Element, Array<Scope>>;
10
19
  private eventDelegator: EventDelegator | undefined;
11
20
 
12
- private vnodesBeingRemoved: Array<VNode>;
21
+ private vnodesBeingRemoved: Array<Element>;
13
22
 
14
23
  constructor() {
15
- this.namespaceByElement = new Map<Element, Array<Scope>>();
24
+ this.namespaceByElement = new WeakMap<Element, Array<Scope>>();
16
25
  this.vnodesBeingRemoved = [];
17
26
  }
18
27
 
@@ -22,22 +31,23 @@ export class IsolateModule {
22
31
 
23
32
  private insertElement(namespace: Array<Scope>, el: Element): void {
24
33
  this.namespaceByElement.set(el, namespace);
25
- this.namespaceTree.set(namespace, el);
34
+ this.namespaceTree.get(namespace, () => new Set())!.add(el);
26
35
  }
27
36
 
37
+ // the element keeps its scope (a leaving element under a Transition still is the
38
+ // child's); only the scope's set of root elements forgets it
28
39
  private removeElement(elm: Element): void {
29
- this.namespaceByElement.delete(elm);
30
- const namespace = this.getNamespace(elm);
31
- if (namespace) {
32
- this.namespaceTree.delete(namespace);
40
+ const namespace = this.namespaceByElement.get(elm);
41
+ const els = namespace && this.namespaceTree.get(namespace);
42
+ if (els) {
43
+ els.delete(elm);
44
+ // P45-A: an unmounted scope leaves no node behind
45
+ if (!els.size) this.namespaceTree.delete(namespace!);
33
46
  }
34
47
  }
35
48
 
36
- public getElement(
37
- namespace: Array<Scope>,
38
- max?: number
39
- ): Element | undefined {
40
- return this.namespaceTree.get(namespace, undefined, max);
49
+ public getElements(namespace: Array<Scope>): Array<Element> {
50
+ return [...(this.namespaceTree.get(namespace) || [])];
41
51
  }
42
52
 
43
53
  public getRootElement(elm: Element): Element | undefined {
@@ -45,13 +55,17 @@ export class IsolateModule {
45
55
  return elm;
46
56
  }
47
57
 
58
+ // G-356 (D196): an element a hook moved out of its component's DOM (a toaster region
59
+ // re-parented into an open modal <dialog>) names where it belongs as `__sygnalHome` (an
60
+ // element of its component), so it stays in its component's scope. With no scope root above
61
+ // elm (moved without one, or out of the app) the event has no scope here (before: threw
62
+ // 'No root element found'). G-381: upOf guards against a home cycle
48
63
  let curr = elm;
64
+ const c = {};
49
65
  while (!this.namespaceByElement.has(curr)) {
50
- curr = curr.parentNode as Element;
51
- if (!curr) {
66
+ curr = upOf(curr, c);
67
+ if (!curr || curr.tagName === 'HTML') {
52
68
  return undefined;
53
- } else if (curr.tagName === 'HTML') {
54
- throw new Error('No root element found, this should not happen at all');
55
69
  }
56
70
  }
57
71
  return curr;
@@ -67,14 +81,35 @@ export class IsolateModule {
67
81
 
68
82
  public createModule() {
69
83
  const self = this;
84
+ // G-564: a vnode can be in the old tree and the new one (a component's cached vnode, a
85
+ // Collection item's copy, a hoisted vnode). When an ancestor is recreated (`alt ? <ol>{c}</ol>
86
+ // : <ul>{c}</ul>`, a new key), snabbdom creates it again and gives it the new element before it
87
+ // destroys the old subtree, so destroy hooks saw the new element (a widget inside wasn't
88
+ // unmounted, its scope lost the new element). A created element notes its children's elements
89
+ // (M: a child with one is mounted, or was), before they are created; a destroyed vnode gives
90
+ // its noted children their old elements back for their destroy hooks (and theirs, down the
91
+ // subtree), and they get the new ones again when the removal starts (S), before the insert
92
+ // hooks. (A vnode that moves to another parent still gives its own destroy hook the new one)
93
+ const M = new Map<any, any>(), S: any[] = [];
94
+ // G-573: a vnode in the new tree twice: its first note (its element before the patch) wins.
95
+ // A destroy hook that throws ends the patch before remove / post: the next patch first gives
96
+ // the vnodes their new elements back and forgets the notes (pre)
97
+ const back = (c: any) => M.has(c) && (S.push(c, c.elm), c.elm = M.get(c));
98
+ const ret = () => { for (let e; S.length; ) e = S.pop(), S.pop().elm = e; };
70
99
  return {
100
+ pre() {
101
+ ret();
102
+ M.clear();
103
+ },
104
+
71
105
  create(emptyVNode: VNode, vNode: VNode) {
72
- const {elm, data = {}} = vNode;
106
+ const {elm, data = {}, children} = vNode;
73
107
  const namespace: Array<Scope> = (data as any).isolate;
74
108
 
75
109
  if (Array.isArray(namespace)) {
76
110
  self.insertElement(namespace, elm as Element);
77
111
  }
112
+ children?.forEach((c: any) => c?.elm && M.set(c, M.get(c) || c.elm));
78
113
  },
79
114
 
80
115
  update(oldVNode: VNode, vNode: VNode) {
@@ -93,32 +128,27 @@ export class IsolateModule {
93
128
  }
94
129
  },
95
130
 
131
+ // (the elements as they are now: post runs after the vnodes got their new ones back)
96
132
  destroy(vNode: VNode) {
97
- self.vnodesBeingRemoved.push(vNode);
133
+ vNode.children?.forEach(back);
134
+ self.vnodesBeingRemoved.push(vNode.elm as Element);
98
135
  },
99
136
 
100
137
  remove(vNode: VNode, cb: Function) {
101
- self.vnodesBeingRemoved.push(vNode);
138
+ self.vnodesBeingRemoved.push(vNode.elm as Element);
139
+ ret();
102
140
  cb();
103
141
  },
104
142
 
105
143
  post() {
106
- const vnodesBeingRemoved = self.vnodesBeingRemoved;
107
- for (let i = vnodesBeingRemoved.length - 1; i >= 0; i--) {
108
- const vnode = vnodesBeingRemoved[i];
109
- const namespace =
110
- vnode.data !== undefined
111
- ? (vnode.data as any).isolation
112
- : undefined;
113
- if (namespace !== undefined) {
114
- self.removeElement(namespace);
115
- }
116
- (self.eventDelegator as EventDelegator).removeElement(
117
- vnode.elm as Element,
118
- namespace
119
- );
144
+ // G-144: a removed root element is no longer one of its scope's roots (an element no
145
+ // scope has as a root isn't in any set: removing it does nothing)
146
+ for (const elm of self.vnodesBeingRemoved) {
147
+ self.removeElement(elm);
148
+ (self.eventDelegator as EventDelegator).removeElement(elm);
120
149
  }
121
150
  self.vnodesBeingRemoved = [];
151
+ M.clear();
122
152
  },
123
153
  };
124
154
  }
@@ -8,7 +8,7 @@ import {BodyDOMSource} from './BodyDOMSource';
8
8
  import {VNode} from './snabbdom';
9
9
  import {enrichEventStream, EnrichedEventStream} from './enrichEventStream';
10
10
  import {ElementFinder} from './ElementFinder';
11
- import {makeIsolateSink, getScopeObj, Scope, IsolateSink} from './isolate';
11
+ import {makeIsolateSink, makeIsolateValue, getScopeObj, Scope, IsolateSink} from './isolate';
12
12
  import {IsolateModule} from './IsolateModule';
13
13
  import {EventDelegator} from './EventDelegator';
14
14
  // [diagnostics hook] no-op when diagnostics are off
@@ -38,6 +38,7 @@ export class MainDOMSource {
38
38
  source._name
39
39
  );
40
40
  this.isolateSink = makeIsolateSink(this._namespace) as any;
41
+ this.isolateValue = makeIsolateValue(this._namespace);
41
42
  }
42
43
 
43
44
  private _elements(): Stream<Array<Element>> {
@@ -79,7 +80,9 @@ export class MainDOMSource {
79
80
  selector: T
80
81
  ): SpecialSelector[T];
81
82
  public select(selector: string): MainDOMSource;
82
- public select(selector: string): DOMSource {
83
+ public select(selector: any): DOMSource {
84
+ // CT-1: a control (controls()) selects its elements by its marker, [data-control="<Key>"]
85
+ if (selector?.__sygnalControl) selector = '' + selector;
83
86
  if (typeof selector !== 'string') {
84
87
  throw new Error(
85
88
  `DOM driver's select() expects the argument to be a ` +
@@ -115,6 +118,9 @@ export class MainDOMSource {
115
118
  options?: EventsFnOptions,
116
119
  bubbles?: boolean
117
120
  ): EnrichedEventStream<HTMLElementEventMap[K]>;
121
+ // PLAN-5 0-S6: any other name (a custom element's 'wa-hover', a widget's or element's
122
+ // CustomEvent): the overload above only typed the built-in names (types only)
123
+ public events(eventType: string, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<Event>;
118
124
  public events(
119
125
  eventType: string,
120
126
  options: EventsFnOptions = {},
@@ -144,4 +150,5 @@ export class MainDOMSource {
144
150
 
145
151
  public isolateSource: (source: MainDOMSource, scope: string) => MainDOMSource;
146
152
  public isolateSink: IsolateSink<VNode>;
153
+ public isolateValue: (node: any, scope: string) => any;
147
154
  }
@@ -20,13 +20,15 @@ export default class PriorityQueue<T> {
20
20
  }
21
21
  }
22
22
 
23
- public delete(t: T): void {
23
+ // returns how many are left
24
+ public delete(t: T): number {
24
25
  for (let i = 0; i < this.arr.length; i++) {
25
26
  if (this.arr[i] === t) {
26
27
  this.arr.splice(i, 1);
27
28
  this.prios.splice(i, 1);
28
- return;
29
+ break;
29
30
  }
30
31
  }
32
+ return this.arr.length;
31
33
  }
32
34
  }
@@ -1,56 +1,30 @@
1
- type Node<Payload> = [Payload | undefined, InternalTree<Payload>];
1
+ // [payload, children, number of children]: the count makes delete's "is this node empty" O(1)
2
+ // (G-290: Object.keys(children) per removal made removing n siblings O(n²))
3
+ type Node<Payload> = [Payload | undefined, InternalTree<Payload>, number];
2
4
 
3
5
  interface InternalTree<Payload> {
4
6
  [name: string]: Node<Payload>;
5
7
  }
6
8
 
7
9
  export default class SymbolTree<Payload, T> {
8
- private tree: Node<Payload> = [undefined, {}];
10
+ private tree: Node<Payload> = [, {}, 0];
9
11
 
10
12
  constructor(private mapper: (t: T) => string) {}
11
13
 
12
- public set(path: Array<T>, element: Payload | undefined, max?: number): void {
13
- let curr = this.tree;
14
- const _max = max !== undefined ? max : path.length;
15
- for (let i = 0; i < _max; i++) {
16
- const n = this.mapper(path[i]);
17
- let child: Node<Payload> = curr[1][n];
18
- if (!child) {
19
- child = [undefined, {}];
20
- curr[1][n] = child;
21
- }
22
- curr = child;
23
- }
24
- curr[0] = element;
25
- }
26
-
27
- public getDefault(
28
- path: Array<T>,
29
- mkDefaultElement: () => Payload,
30
- max?: number
31
- ): Payload {
32
- return this.get(path, mkDefaultElement, max) as Payload;
33
- }
34
-
35
14
  public get(
36
15
  path: Array<T>,
37
16
  mkDefaultElement?: () => Payload,
38
- max?: number
17
+ max = path.length
39
18
  ): Payload | undefined {
40
19
  let curr = this.tree;
41
- const _max = max !== undefined ? max : path.length;
42
- for (let i = 0; i < _max; i++) {
20
+ for (let i = 0; i < max; i++) {
43
21
  const n = this.mapper(path[i]);
44
- let child: Node<Payload> = curr[1][n];
45
- if (!child) {
46
- if (mkDefaultElement) {
47
- child = [undefined, {}];
48
- curr[1][n] = child;
49
- } else {
50
- return undefined;
51
- }
22
+ if (!curr[1][n]) {
23
+ if (!mkDefaultElement) return undefined;
24
+ curr[2]++; // G-290: count the children
25
+ curr[1][n] = [, {}, 0];
52
26
  }
53
- curr = child;
27
+ curr = curr[1][n];
54
28
  }
55
29
  if (mkDefaultElement && !curr[0]) {
56
30
  curr[0] = mkDefaultElement();
@@ -58,15 +32,12 @@ export default class SymbolTree<Payload, T> {
58
32
  return curr[0];
59
33
  }
60
34
 
61
- public delete(path: Array<T>): void {
62
- let curr = this.tree;
63
- for (let i = 0; i < path.length - 1; i++) {
64
- const child = curr[1][this.mapper(path[i])];
65
- if (!child) {
66
- return;
67
- }
68
- curr = child;
69
- }
70
- delete curr[1][this.mapper(path[path.length - 1])];
35
+ // P45-A: drop the payload at path and prune the nodes left empty; true when node is empty
36
+ public delete(path: Array<T>, max = path.length, node = this.tree, i = 0): boolean {
37
+ if (i < max) {
38
+ const k = this.mapper(path[i]), child = node[1][k];
39
+ if (child && this.delete(path, max, child, i + 1)) delete node[1][k], node[2]--;
40
+ } else node[0] = undefined;
41
+ return !node[0] && !node[2];
71
42
  }
72
43
  }
@@ -39,15 +39,19 @@ const isSet = (cn: any) => cn != null && cn !== false;
39
39
  const tokens = (cn: any) => (typeof cn === 'string' ? cn.split(/\s+/).filter(Boolean) : []);
40
40
 
41
41
  function syncClassName(oldVnode: VNode, vnode: VNode): void {
42
- const elm = vnode.elm as any;
43
- if (!elm || !elm.classList) return;
44
42
  const props: any = vnode.data?.props;
45
43
  const oldProps: any = oldVnode.data?.props;
46
44
  const hasKey = !!props && 'className' in props;
45
+ const oldCn = oldProps ? oldProps.className : undefined;
46
+ // P45-A: keyed on the prop (B-012), not the tag: nothing to do when neither vnode has one
47
+ if (!hasKey && !isSet(oldCn)) return;
48
+ const elm = vnode.elm as any;
49
+ if (!elm || !elm.classList) return;
47
50
  const cn = hasKey ? props.className : undefined;
48
51
  if (isSet(cn)) {
49
- // propsModule replaced the whole attribute: put the class={{...}} entries back
50
- if (!oldProps || oldProps.className !== cn) addAll(elm, classObject(vnode));
52
+ // propsModule replaced the whole attribute: put the selector classes and the
53
+ // class={{...}} entries back
54
+ if (!oldProps || oldProps.className !== cn) addAll(elm, baseClasses(vnode));
51
55
  return;
52
56
  }
53
57
  if (hasKey) {
@@ -58,8 +62,6 @@ function syncClassName(oldVnode: VNode, vnode: VNode): void {
58
62
  }
59
63
  return;
60
64
  }
61
- const oldCn = oldProps ? oldProps.className : undefined;
62
- if (!isSet(oldCn)) return;
63
65
  const keep = baseClasses(vnode);
64
66
  for (const name of tokens(oldCn)) {
65
67
  if (keep.indexOf(name) === -1) elm.classList.remove(name);
@@ -69,6 +71,6 @@ function syncClassName(oldVnode: VNode, vnode: VNode): void {
69
71
  }
70
72
 
71
73
  export const classNameModule = {
72
- create: (emptyVnode: VNode, vnode: VNode) => syncClassName(emptyVnode, vnode),
74
+ create: syncClassName,
73
75
  update: syncClassName,
74
76
  };
@@ -9,23 +9,40 @@
9
9
  * Fix: after propsModule has run, compare `value`/`checked` props against the
10
10
  * live element and write them when they differ. Only form fields (INPUT,
11
11
  * TEXTAREA, SELECT; not input type=file) are controlled: `value` compares as a
12
- * string, `checked` as a boolean (1H-7). Other elements with a value prop
13
- * (<progress>, <meter>, custom elements with object values) are left alone.
12
+ * string, `checked` as a boolean (1H-7). A present-but-nullish `value` keeps an
13
+ * input/textarea at '' and a nullish `checked` at false (D49); an absent prop
14
+ * leaves the field to the user. D196: a form-associated custom element is a field
15
+ * too (its value is written as the prop). Other elements with a value prop
16
+ * (<progress>, <meter>, custom elements that aren't form-associated) are left alone.
17
+ *
18
+ * (PLAN-4.6 R6, G-347: G-146's input stamps are gone with the old core: the core renders a
19
+ * state in the flush it is written in, so no render shows an older state than the field.)
14
20
  */
15
21
 
16
22
  import type {VNode} from 'snabbdom/build/vnode.js';
17
23
 
24
+ // P45-A: a form field by its vnode's tag (sel), checked before any other work: most vnodes aren't.
25
+ // D196: a form-associated custom element (a hyphenated tag whose class has `static
26
+ // formAssociated = true`, e.g. Web Awesome's wa-input / wa-rating) is a field too; only a
27
+ // hyphenated tag's element is read
28
+ export const isField = (vnode: VNode): any =>
29
+ /^(input|textarea|select)(?![\w-])/i.test(vnode.sel as string) || /^\w+-/.test(vnode.sel as string) && (vnode.elm as any).constructor.formAssociated;
30
+
18
31
  function syncControlled(oldVnode: VNode, vnode: VNode): void {
32
+ if (!isField(vnode)) return;
19
33
  const props = vnode.data?.props;
20
34
  const elm = vnode.elm as any;
21
- if (!props || !elm || !/^(INPUT|TEXTAREA|SELECT)$/.test(elm.tagName) || elm.type == 'file') return;
35
+ if (!props || !elm || elm.type == 'file') return;
22
36
  const oldProps: any = oldVnode.data?.props || {};
23
37
  const {value, checked} = props;
24
38
  // Only the case propsModule skipped (prop unchanged); it handles real changes.
25
- if (value === oldProps.value && value != null && elm.value !== String(value)) {
26
- elm.value = String(value);
39
+ // A nullish <select> value is left alone: on create the browser shows the first option.
40
+ if ('value' in props && value === oldProps.value && (value != null || elm.tagName != 'SELECT')) {
41
+ const v = value == null ? '' : String(value);
42
+ // a custom element's value can be a number: compared as a string, written as the prop
43
+ if (String(elm.value) !== v) elm.value = elm.constructor.formAssociated ? value : v;
27
44
  }
28
- if (checked === oldProps.checked && checked != null && elm.checked !== !!checked) {
45
+ if ('checked' in props && checked === oldProps.checked && elm.checked !== !!checked) {
29
46
  elm.checked = !!checked;
30
47
  }
31
48
  }
@@ -15,8 +15,16 @@ export interface EnrichedEventStream<T = Event> extends Stream<T> {
15
15
 
16
16
  key(): EnrichedEventStream<string>;
17
17
  key<R>(fn: (key: string) => R): EnrichedEventStream<R>;
18
+
19
+ /** e.detail: a CustomEvent's payload (a widget's emit(name, detail), a web component's event) */
20
+ detail<D = any>(): EnrichedEventStream<D>;
21
+ detail<R, D = any>(fn: (detail: D) => R): EnrichedEventStream<R>;
18
22
  }
19
23
 
24
+ // one enricher: a method that maps each event through `get`, then `fn` when given
25
+ const pick = (s$: any, get: (e: any) => any) => (fn?: (v: any) => any): any =>
26
+ enrichEventStream(s$.map((e: any) => (e = get(e), fn ? fn(e) : e)))
27
+
20
28
  /**
21
29
  * Adds chainable convenience methods to a DOM event stream.
22
30
  *
@@ -26,59 +34,29 @@ export interface EnrichedEventStream<T = Event> extends Stream<T> {
26
34
  * DOM.click('.item').data('id', Number)
27
35
  * DOM.change('.checkbox').checked()
28
36
  * DOM.keydown('.field').key()
37
+ * DOM.select('.due').events('change').detail()
29
38
  */
30
39
  export function enrichEventStream(stream$: any): any {
31
- // .value(fn?) — extract e.target.value
32
- stream$.value = function value(fn?: (val: any) => any): any {
33
- const mapped = stream$.map((e: any) => {
34
- const val = e?.target?.value
35
- return fn ? fn(val) : val
36
- });
37
- return enrichEventStream(mapped);
38
- }
39
-
40
- // .checked(fn?) — extract e.target.checked
41
- stream$.checked = function checked(fn?: (val: any) => any): any {
42
- const mapped = stream$.map((e: any) => {
43
- const val = !!e?.target?.checked
44
- return fn ? fn(val) : val
45
- });
46
- return enrichEventStream(mapped);
47
- }
48
-
49
- // .data(name, fn?) — extract dataset[name] from e.target or its nearest ancestor that has
50
- // it. dataset keys are camelCase but attributes are kebab-case (`taskId` is
51
- // `data-task-id`), so the closest() selector converts (B-028). The name may be given
52
- // either way: `.data('taskId')` and `.data('task-id')` read the same attribute (3E/R3).
53
- stream$.data = function data(name: string, fn?: (val: any) => any): any {
40
+ // .value(fn?) — e.target.value; .checked(fn?) — e.target.checked as a boolean
41
+ stream$.value = pick(stream$, (e: any) => e?.target?.value)
42
+ stream$.checked = pick(stream$, (e: any) => !!e?.target?.checked)
43
+ // .data(name, fn?) — dataset[name] from e.target or its nearest ancestor that has it.
44
+ // dataset keys are camelCase but attributes are kebab-case (`taskId` is `data-task-id`), so the
45
+ // closest() selector converts (B-028). The name may be given either way: `.data('taskId')` and
46
+ // `.data('task-id')` read the same attribute (3E/R3).
47
+ stream$.data = (name: string, fn?: (v: any) => any): any => {
54
48
  const key = String(name).replace(/-([a-z])/g, (_, c) => c.toUpperCase())
55
49
  const attr = `[data-${key.replace(/[A-Z]/g, c => '-' + c.toLowerCase())}]`
56
- const mapped = stream$.map((e: any) => {
50
+ return pick(stream$, (e: any) => {
57
51
  const t = e?.target
58
52
  const el = typeof t?.closest == 'function' ? t.closest(attr) || t : t
59
- const val = el?.dataset?.[key]
60
- return fn ? fn(val) : val
61
- });
62
- return enrichEventStream(mapped);
53
+ return el?.dataset?.[key]
54
+ })(fn)
63
55
  }
64
-
65
- // .target(fn?) — extract e.target
66
- stream$.target = function target(fn?: (el: any) => any): any {
67
- const mapped = stream$.map((e: any) => {
68
- const el = e?.target
69
- return fn ? fn(el) : el
70
- });
71
- return enrichEventStream(mapped);
72
- }
73
-
74
- // .key(fn?) — extract e.key (keyboard events)
75
- stream$.key = function key(fn?: (val: any) => any): any {
76
- const mapped = stream$.map((e: any) => {
77
- const val = e?.key
78
- return fn ? fn(val) : val
79
- });
80
- return enrichEventStream(mapped);
81
- }
82
-
56
+ // .target(fn?) — e.target; .key(fn?) — e.key (keyboard events)
57
+ stream$.target = pick(stream$, (e: any) => e?.target)
58
+ stream$.key = pick(stream$, (e: any) => e?.key)
59
+ // .detail(fn?) — e.detail (CustomEvents: widgets' emit(), web components) (PLAN-5 W-3)
60
+ stream$.detail = pick(stream$, (e: any) => e?.detail)
83
61
  return stream$
84
62
  }
@@ -0,0 +1,7 @@
1
+ import {Fragment as _Fragment} from 'snabbdom/build/jsx.js';
2
+
3
+ // Tag Fragment so we can identify it even after minification mangles Function.name.
4
+ // Its own module so the JSX runtime entries (which keep snabbdom external, D188) take it
5
+ // without the rest of the snabbdom barrel
6
+ (_Fragment as any).__sygnalFragment = true;
7
+ export const Fragment = _Fragment;
@@ -15,37 +15,40 @@ export type IsolateSink<T extends VNode> = (
15
15
  export function makeIsolateSink<T extends VNode>(
16
16
  namespace: Array<Scope>
17
17
  ): IsolateSink<T> {
18
- return (sink, scope) => {
19
- if (scope === ':root') {
20
- return sink;
21
- }
18
+ const value = makeIsolateValue(namespace);
19
+ return (sink, scope) => scope === ':root' ? sink : sink.map(node => value(node, scope) as T);
20
+ }
22
21
 
23
- return sink.map(node => {
24
- if (!node) {
25
- return node;
26
- }
27
- const scopeObj = getScopeObj(scope);
28
- const newNode = {
29
- ...(node as any),
30
- data: {
31
- ...node.data,
32
- isolate:
33
- !node.data || !Array.isArray((node.data as any).isolate)
34
- ? namespace.concat([scopeObj])
35
- : (node.data as any).isolate,
36
- },
37
- };
38
- return {
39
- ...newNode,
40
- key:
41
- newNode.key !== undefined
42
- ? newNode.key
43
- : JSON.stringify(newNode.data.isolate),
44
- } as T;
45
- });
22
+ /**
23
+ * PLAN-4.6: the value-level isolateSink (the core scopes each instance's vnode without a
24
+ * stream per instance): the same mapping, applied to one vnode
25
+ */
26
+ export function makeIsolateValue(namespace: Array<Scope>): (node: any, scope: string) => any {
27
+ return (node, scope) => {
28
+ if (scope === ':root' || !node) {
29
+ return node;
30
+ }
31
+ const isolate = namespace.concat([getScopeObj(scope)]);
32
+ const newNode = scoped(node, isolate);
33
+ return {
34
+ ...newNode,
35
+ key:
36
+ newNode.key !== undefined
37
+ ? newNode.key
38
+ : JSON.stringify((newNode.data && newNode.data.isolate) || isolate),
39
+ };
46
40
  };
47
41
  }
48
42
 
43
+ // G-144: a fragment has no element of its own, so each of its top-level elements (through
44
+ // nested fragments) carries the scope; the fragment keeps the key
45
+ function scoped(node: any, isolate: Array<Scope>): any {
46
+ return !node || typeof node != 'object' ? node
47
+ : node.sel ? {...node, data: {...node.data, isolate: node.data && Array.isArray(node.data.isolate) ? node.data.isolate : isolate}}
48
+ : node.children ? {...node, children: node.children.map((c: any) => scoped(c, isolate))}
49
+ : node;
50
+ }
51
+
49
52
  export function getScopeObj(scope: string): Scope {
50
53
  return {
51
54
  type: isClassOrId(scope) ? 'sibling' : 'total',