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.
- package/CHANGELOG.md +646 -0
- package/README.md +24 -9
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +362 -91
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +362 -92
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3032 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3032 -296
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +13178 -5769
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2908 -276
- package/dist/index.esm.js +13149 -5779
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +323 -88
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +323 -89
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +166 -103
- package/package.json +98 -14
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +19 -3
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +101 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +1 -1
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +97 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +98 -7
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +66 -10
- package/src/extra/diagnostics/codes.ts +268 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +1 -1
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +90 -217
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +2433 -175
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/index.d.ts +2631 -117
- package/src/index.ts +25 -4
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +256 -133
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -121
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +22 -3
- package/src/vite/plugin.ts +222 -24
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {Driver} from '../run/types';
|
|
2
|
-
import {init, Module, Options as SnabbdomOptions, VNode
|
|
2
|
+
import {init, Module, Options as SnabbdomOptions, VNode} from './snabbdom';
|
|
3
3
|
import xs, {Stream, Listener} from 'xstream';
|
|
4
|
-
import {concat, sampleCombine} from '../../extra/xstreamExtras';
|
|
5
4
|
import {MainDOMSource} from './MainDOMSource';
|
|
6
5
|
import {VNodeWrapper} from './VNodeWrapper';
|
|
7
|
-
import {getValidNode, checkValidContainer} from './utils';
|
|
6
|
+
import {getValidNode, checkValidContainer, POKE, flat} from './utils';
|
|
8
7
|
import defaultModules from './modules';
|
|
9
8
|
import {IsolateModule} from './IsolateModule';
|
|
10
9
|
import {EventDelegator} from './EventDelegator';
|
|
@@ -36,17 +35,11 @@ export interface DOMDriverOptions {
|
|
|
36
35
|
snabbdomOptions?: SnabbdomOptions;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
function dropCompletion<T>(input: Stream<T>): Stream<T> {
|
|
40
|
-
return xs.merge(input, xs.never());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
38
|
function unwrapElementFromVNode(vnode: VNode): Element {
|
|
44
39
|
return vnode.elm as Element;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
(console.error || console.log)(err);
|
|
49
|
-
}
|
|
42
|
+
const ERR = 'data-sygnal-error';
|
|
50
43
|
|
|
51
44
|
function makeDOMReady$(): Stream<null> {
|
|
52
45
|
return xs.create<null>({
|
|
@@ -68,11 +61,64 @@ function makeDOMReady$(): Stream<null> {
|
|
|
68
61
|
});
|
|
69
62
|
}
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
// G-456 (D217): the app's first patch adopts markup already in the mount point (server HTML, an
|
|
65
|
+
// Astro island, a Vike page). Its old vnode is built from the DOM with the client's vnode as the
|
|
66
|
+
// template, by position (no toVNode): an element with the client's tag takes the client's
|
|
67
|
+
// selector and key (so component roots and Collection items match), and its data is what the
|
|
68
|
+
// modules need to reach the client's from what the server wrote: the dataset (stale keys go:
|
|
69
|
+
// data-sygnal-ssr), the class as the className prop (the className module drops stale classes),
|
|
70
|
+
// and each attribute the client sets as an attribute or a prop, with the server's value (nothing
|
|
71
|
+
// is written again: an iframe's src isn't reloaded). Any other attribute is removed now; so is a
|
|
72
|
+
// style (G-483: the style module writes the client's declarations, so none of the server's
|
|
73
|
+
// stays). A single text child keeps its text node. Whitespace and comments where the client has
|
|
74
|
+
// no text go; any other node that doesn't match stays as a placeholder that snabbdom replaces
|
|
75
|
+
// with the client's node, in place (the lists stay aligned, so the nodes after it are still
|
|
76
|
+
// adopted). Made again that way, as a fresh render makes them: an element whose selector has a
|
|
77
|
+
// class or id (G-486: `h('p.card')`, the Portal placeholder; patching never corrects them), and
|
|
78
|
+
// one whose hook expects a new element (G-485): a create or init hook (a thunk), an insert hook
|
|
79
|
+
// without a postpatch (a Transition's enter, a measured row, the toaster region, a lazy()
|
|
80
|
+
// placeholder), and any insert hook of the user's own (G-521: with a postpatch too; `u` when a
|
|
81
|
+
// ref, autoFocus or widget chained theirs after it; `s` marks a function chainHooks made)
|
|
82
|
+
const adopt = (e: any, v: any): any => {
|
|
83
|
+
// (a hole, `{cond && <X/>}`, has no node: snabbdom skips it. G-481: no fragment either, they
|
|
84
|
+
// are flat by now: their nodes pair with their children in their parent's run)
|
|
85
|
+
const c = (v.children || []).filter((z: any) => z), out: any[] = [];
|
|
86
|
+
for (let j = 0, x = e.firstChild, y: any; x; x = y) {
|
|
87
|
+
const w = c[j], t = x.nodeType, d = w?.data || {}, k = d.hook;
|
|
88
|
+
// a node that doesn't match stays in its place under a selector no vnode has: snabbdom makes
|
|
89
|
+
// the client's node before it and removes it (the lists stay aligned: no other node is paired)
|
|
90
|
+
let n: any = {sel: '', data: {}, elm: x};
|
|
91
|
+
// G-520: client text vnodes next to each other (`Hello, {name}!`) are one server text node:
|
|
92
|
+
// it is split by the client's text when that is its start, so the nodes after stay paired
|
|
93
|
+
t == 3 && c[j + 1]?.text != null && !c[j + 1].sel && w?.text && x.data.startsWith(w.text) && x.splitText(w.text.length);
|
|
94
|
+
y = x.nextSibling;
|
|
95
|
+
if (t == 3 && w && !w.sel && w.text != null) n = {text: x.data, elm: x};
|
|
96
|
+
else if (t == 1 && x.localName == w?.sel && !(k && (k.create || k.init || k.u || k.insert && !(k.postpatch && k.insert.s)))) {
|
|
97
|
+
const p = d.props || {}, a = d.attrs || {}, o: any = {}, at: any = {}, pr: any = {}, f = x.firstChild;
|
|
98
|
+
o.class = 'className';
|
|
99
|
+
for (const m in p) o[m == 'htmlFor' ? 'for' : m.toLowerCase()] = m;
|
|
100
|
+
// G-482: a data-* attribute the client sets as an attribute isn't in the dataset (the
|
|
101
|
+
// dataset module would remove it): it goes, and the attributes module writes it again.
|
|
102
|
+
// G-487: `open` (a <details> the user opened) stays when the client doesn't render it
|
|
103
|
+
for (const {name: m, value: y} of [...x.attributes])
|
|
104
|
+
/^data-/.test(m) ? m in a && x.removeAttribute(m)
|
|
105
|
+
: m in a ? at[m] = y
|
|
106
|
+
: m == 'open' ? 0
|
|
107
|
+
: m in o && !d.ns ? pr[o[m]] = y
|
|
108
|
+
: x.removeAttribute(m);
|
|
109
|
+
// G-484: a textarea's value no longer follows its text, which goes
|
|
110
|
+
x.localName == 'textarea' && (x.value = x.value);
|
|
111
|
+
const s = w.text != null && f?.nodeType == 3 && !f.nextSibling;
|
|
112
|
+
n = {sel: w.sel, key: w.key, data: {dataset: {...x.dataset}, attrs: at, props: pr}, children: s ? undefined : adopt(x, w), text: s ? f.data : undefined, elm: x};
|
|
113
|
+
} else if (t != 1 && (t != 3 || !/\S/.test(x.data))) {
|
|
114
|
+
x.remove();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
out.push(n);
|
|
118
|
+
j++;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
};
|
|
76
122
|
|
|
77
123
|
function makeDOMDriver(
|
|
78
124
|
container: string | Element | DocumentFragment,
|
|
@@ -86,66 +132,79 @@ function makeDOMDriver(
|
|
|
86
132
|
const patch = init([isolateModule.createModule() as Partial<Module>].concat(modules), undefined, snabbdomOptions);
|
|
87
133
|
const domReady$ = makeDOMReady$();
|
|
88
134
|
let vnodeWrapper: VNodeWrapper;
|
|
89
|
-
let mutationObserver: MutationObserver;
|
|
90
|
-
const mutationConfirmed$ = xs.create<null>({
|
|
91
|
-
start(listener) {
|
|
92
|
-
mutationObserver = new MutationObserver(() => listener.next(null));
|
|
93
|
-
},
|
|
94
|
-
stop() {
|
|
95
|
-
mutationObserver.disconnect();
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
135
|
|
|
99
136
|
function DOMDriver(vnode$: Stream<VNode>, name = 'DOM'): MainDOMSource {
|
|
100
137
|
domDriverInputGuard(vnode$);
|
|
101
138
|
const sanitation$ = xs.create<null>();
|
|
102
139
|
|
|
140
|
+
let r: any;
|
|
103
141
|
const firstRoot$ = domReady$.map(() => {
|
|
104
142
|
const firstRoot = getValidNode(container) || document.body;
|
|
105
143
|
vnodeWrapper = new VNodeWrapper(firstRoot);
|
|
144
|
+
r = {sel: '', data: {isolate: []}, elm: firstRoot};
|
|
106
145
|
return firstRoot;
|
|
107
146
|
});
|
|
108
147
|
|
|
109
148
|
const rememberedVNode$ = vnode$.remember();
|
|
110
149
|
rememberedVNode$.addListener({});
|
|
111
150
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
// P45-C (rec 9, D146): the root element, when the DOM is ready and then after each patch
|
|
152
|
+
// (it was a MutationObserver on the root's subtree: DOM changes made outside a patch no
|
|
153
|
+
// longer emit)
|
|
154
|
+
// G-261: and when Sygnal changes the DOM outside a patch (a Transition's leave, a Portal
|
|
155
|
+
// mounted late): an event that bubbles up to the root. G-276: on the current root (a patch
|
|
156
|
+
// can replace the first one). G-277: the innermost app's root takes it (a nested app or
|
|
157
|
+
// custom element doesn't re-emit the outer app's DOM source)
|
|
158
|
+
let pl: any, cur: any;
|
|
159
|
+
const poke = (e: Event) => pl && (e.stopPropagation(), pl.next(cur));
|
|
160
|
+
const off = () => cur?.removeEventListener(POKE, poke);
|
|
161
|
+
// G-545: run() gives any DOM driver its reporter (onError, phase 'patch') on the isolate
|
|
162
|
+
// module; an explicit reportSnabbdomError comes first
|
|
163
|
+
const rep = (e: any) => (options.reportSnabbdomError || (isolateModule as any).rep || console.error)(e);
|
|
164
|
+
let dead = 0, end = 0;
|
|
165
|
+
const rootElement$ = firstRoot$
|
|
115
166
|
.map(
|
|
116
|
-
firstRoot =>
|
|
117
|
-
xs
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
(firstRoot, P = (o: any, v: any) => (v = flat(v), patch(o == r ? (cur.removeAttribute?.(ERR), {...r, sel: v.sel, key: v.key, children: adopt(firstRoot, v)}) : o, v))) =>
|
|
168
|
+
xs.merge(
|
|
169
|
+
xs
|
|
170
|
+
.merge(rememberedVNode$.endWhen(sanitation$), sanitation$.map(() => (end = 1, null)))
|
|
171
|
+
.map(vnode => vnodeWrapper.call(vnode))
|
|
172
|
+
// the first step gives the root its scope; the second, the app's first patch,
|
|
173
|
+
// adopts the markup in it (G-456). The root keeps its own attributes (G-466)
|
|
174
|
+
.startWith(r)
|
|
175
|
+
// G-540: a patch that throws (a hook, a module) is reported once, and the app's DOM
|
|
176
|
+
// stops updating: snabbdom stopped half way (some elements patched, created or
|
|
177
|
+
// removed, the rest not; insert hooks not run), and no vnode tree matches that DOM
|
|
178
|
+
// (patching on from either tree, or adopting the DOM again, left wrong or duplicated
|
|
179
|
+
// DOM). The stream doesn't end (its events, the root element), and a patch to no
|
|
180
|
+
// children (dispose) still runs, so what the last good tree mounted (a Portal's
|
|
181
|
+
// content) is removed; its errors aren't reported again. G-549: only dispose (`end`:
|
|
182
|
+
// the sanitation signal), not a live render that has no children. G-543 (D224): the
|
|
183
|
+
// root element is marked data-sygnal-error="patch" while its DOM is stopped (CSS can
|
|
184
|
+
// show it; dispose removes it). G-550: marked before the error is reported (onError
|
|
185
|
+
// can read it); the first patch clears a mark a failed, undisposed app left in the
|
|
186
|
+
// container, and dispose removes it only when this app set it; a throw while
|
|
187
|
+
// flattening the tree (a key JSON.stringify can't take) is a patch error too (P
|
|
188
|
+
// flattens). A DocumentFragment mount point has no attributes: no mark
|
|
189
|
+
.fold((o: any, v: any) => {
|
|
190
|
+
if (!dead || end) try { o = P(o, v) } catch (e) { dead++ || (cur.setAttribute?.(ERR, 'patch'), rep(e)) }
|
|
191
|
+
end && dead && cur.removeAttribute?.(ERR)
|
|
192
|
+
return o
|
|
193
|
+
}, {...r, data: {}})
|
|
194
|
+
.drop(1)
|
|
195
|
+
.map(unwrapElementFromVNode)
|
|
196
|
+
.startWith(firstRoot as any)
|
|
197
|
+
.map((el: any) => (off(), el.addEventListener(POKE, poke), cur = el)),
|
|
198
|
+
// never completes (nor does the root element). G-285: the listener moves with the
|
|
199
|
+
// root and goes when the app stops (a disposed app's driver isn't kept by its root)
|
|
200
|
+
xs.create<any>({start: l => pl = l, stop: () => (pl = 0, off())})
|
|
201
|
+
)
|
|
137
202
|
)
|
|
138
|
-
.flatten()
|
|
139
|
-
|
|
140
|
-
const rootElement$ = concat(domReady$, mutationConfirmed$)
|
|
203
|
+
.flatten()
|
|
141
204
|
.endWhen(sanitation$)
|
|
142
|
-
.compose(sampleCombine(elementAfterPatch$))
|
|
143
|
-
.map(arr => arr[1])
|
|
144
205
|
.remember();
|
|
145
206
|
|
|
146
|
-
rootElement$.addListener({
|
|
147
|
-
error: options.reportSnabbdomError || defaultReportSnabbdomError,
|
|
148
|
-
});
|
|
207
|
+
rootElement$.addListener({error: rep});
|
|
149
208
|
|
|
150
209
|
const delegator = new EventDelegator(rootElement$, isolateModule);
|
|
151
210
|
|
|
@@ -95,7 +95,15 @@ export class MockedDOMSource {
|
|
|
95
95
|
return out;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
public select(selector:
|
|
98
|
+
public select(selector: any): MockedDOMSource {
|
|
99
|
+
// CT-1: a control selects by its marker; a component (any other function) is rejected
|
|
100
|
+
// like the real DOM source does (SYG124 in the dev entry names the fixes)
|
|
101
|
+
if (typeof selector == 'function') {
|
|
102
|
+
if (!selector.__sygnalControl) {
|
|
103
|
+
throw new Error(`DOM driver's select() expects the argument to be a string as a CSS selector`);
|
|
104
|
+
}
|
|
105
|
+
selector = '' + selector;
|
|
106
|
+
}
|
|
99
107
|
const mockConfigForSelector = this._mockConfig[selector] || {};
|
|
100
108
|
|
|
101
109
|
return new MockedDOMSource(
|
|
@@ -113,6 +121,18 @@ export class MockedDOMSource {
|
|
|
113
121
|
return source.select('.' + SCOPE_PREFIX + scope);
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
/**
|
|
125
|
+
* PLAN-4.6: isolateSink for one vnode (a copy, with the scope class). 4-I G-558: a fragment (a
|
|
126
|
+
* Collection's, `<>…</>`) has no element: each of its top-level elements gets the class (as the
|
|
127
|
+
* real driver's scoped()); a text vnode is left as it is
|
|
128
|
+
*/
|
|
129
|
+
public isolateValue(vnode: any, scope: string): any {
|
|
130
|
+
// G-305: the scope's class token exactly (scope s1 is not s14)
|
|
131
|
+
return !vnode || typeof vnode != 'object' ? vnode
|
|
132
|
+
: !vnode.sel ? (vnode.children ? {...vnode, children: vnode.children.map((c: any) => this.isolateValue(c, scope))} : vnode)
|
|
133
|
+
: vnode.sel.split('.').indexOf(SCOPE_PREFIX + scope) > 0 ? vnode : {...vnode, sel: vnode.sel + `.${SCOPE_PREFIX}${scope}`};
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
public isolateSink(sink: any, scope: string): any {
|
|
117
137
|
return adapt(
|
|
118
138
|
xs.fromObservable<any>(sink).map((vnode: VNode) => {
|
package/src/cycle/dom/modules.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
import {Module, classModule,
|
|
1
|
+
import {Module, classModule, attributesModule, datasetModule} from './snabbdom';
|
|
2
2
|
import {styleModule} from './styleModule';
|
|
3
3
|
import {selectModule} from './selectModule';
|
|
4
4
|
import {controlledInputModule} from './controlledInputModule';
|
|
5
5
|
import {classNameModule} from './classNameModule';
|
|
6
|
+
import {propsModule} from './propsModule';
|
|
7
|
+
import {isField} from './controlledInputModule';
|
|
8
|
+
import {sameData} from './utils';
|
|
6
9
|
|
|
7
|
-
const
|
|
10
|
+
const list: Array<Module> = [
|
|
8
11
|
styleModule,
|
|
9
12
|
classModule,
|
|
10
|
-
propsModule,
|
|
13
|
+
propsModule, // Sygnal's: also clears removed and nullish props (B-015, G-109)
|
|
11
14
|
classNameModule, // after propsModule (B-012)
|
|
12
15
|
attributesModule,
|
|
13
16
|
datasetModule,
|
|
14
17
|
selectModule,
|
|
15
18
|
controlledInputModule,
|
|
16
19
|
];
|
|
20
|
+
// P46-P: one update hook for these modules, in their order. A vnode whose data is the same as
|
|
21
|
+
// last time (each bucket shallow-equal) needs none of them, unless it is a form field (the
|
|
22
|
+
// controlled-input and select modules re-sync it every patch)
|
|
23
|
+
const ups = list.map(m => m.update!);
|
|
24
|
+
const modules: Array<Module> = list.map(({update, ...m}) => m as Module).concat({
|
|
25
|
+
update(o: any, v: any) {
|
|
26
|
+
if (!isField(v) && sameData(o.data, v.data)) return;
|
|
27
|
+
for (let i = 0; i < ups.length; i++) ups[i](o, v);
|
|
28
|
+
},
|
|
29
|
+
} as Module);
|
|
17
30
|
|
|
18
31
|
export {styleModule, classModule, propsModule, attributesModule, datasetModule, selectModule, controlledInputModule, classNameModule};
|
|
19
32
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sygnal's props module: snabbdom's propsModule, plus clearing removed and
|
|
3
|
+
* nullish props (B-015, G-109).
|
|
4
|
+
*
|
|
5
|
+
* Problem: snabbdom's propsModule writes new and changed props but never
|
|
6
|
+
* unsets a prop that disappears, so a reused element keeps `title`, `disabled`,
|
|
7
|
+
* `href`, ... from the previous render. A nullish prop is written as the
|
|
8
|
+
* string "undefined"/"null" (`title={cond ? 'x' : undefined}`, `src={null}`).
|
|
9
|
+
*
|
|
10
|
+
* Fix: write new and changed props except nullish ones (G-109: nothing is ever
|
|
11
|
+
* written as "null"). For each prop that is gone (or became nullish), remove
|
|
12
|
+
* the reflected attribute, which resets the property (numbers such as
|
|
13
|
+
* maxLength fall back to their default too). Without such an attribute, reset
|
|
14
|
+
* the property by its current type (boolean -> false, string -> '', other
|
|
15
|
+
* non-number -> undefined/null) if it differs (G-095). `className` is left
|
|
16
|
+
* to classNameModule (B-012). On form fields a nullish `value`/`checked` clears
|
|
17
|
+
* the field ('' / false, D49); a removed one is left alone (the controlled-input
|
|
18
|
+
* module owns them; removing them hands control back to the user, as in React).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {VNode} from 'snabbdom/build/vnode.js';
|
|
22
|
+
|
|
23
|
+
function clearProp(elm: any, key: string, nullish?: boolean): void {
|
|
24
|
+
if (key === 'className') return;
|
|
25
|
+
if (/^(value|checked)$/.test(key) && /^(INPUT|TEXTAREA|SELECT)$/.test(elm.tagName)) {
|
|
26
|
+
const empty = key === 'value' ? '' : false;
|
|
27
|
+
if (nullish && elm[key] !== empty) elm[key] = empty;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const attr = key === 'htmlFor' ? 'for' : key.replace(/^aria(?=[A-Z])/, 'aria-').toLowerCase();
|
|
31
|
+
// G-095: a reflected attribute is just removed, which resets the property. Writing it first
|
|
32
|
+
// (src = '') would queue an img/video error event or navigate an iframe to about:blank.
|
|
33
|
+
if (elm.hasAttribute?.(attr)) return elm.removeAttribute(attr);
|
|
34
|
+
const cur = elm[key];
|
|
35
|
+
const t = typeof cur;
|
|
36
|
+
const reset = t === 'boolean' ? false : t === 'string' ? '' : cur === null || t === 'function' ? null : undefined;
|
|
37
|
+
try {
|
|
38
|
+
// numbers (maxLength, ...) are left to their default; unchanged values aren't rewritten
|
|
39
|
+
if (t !== 'number' && cur !== reset) elm[key] = reset;
|
|
40
|
+
} catch (_) {
|
|
41
|
+
// e.g. contentEditable rejects ''
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateProps(oldVnode: VNode, vnode: VNode): void {
|
|
46
|
+
const elm: any = vnode.elm;
|
|
47
|
+
const props: any = vnode.data?.props || {};
|
|
48
|
+
const oldProps: any = oldVnode.data?.props || {};
|
|
49
|
+
if (!elm || oldProps === props) return;
|
|
50
|
+
for (const key in oldProps) {
|
|
51
|
+
if (!(key in props)) clearProp(elm, key);
|
|
52
|
+
}
|
|
53
|
+
for (const key in props) {
|
|
54
|
+
const cur = props[key];
|
|
55
|
+
if (cur === oldProps[key]) continue;
|
|
56
|
+
// a nullish className is left to classNameModule, which rebuilds the attribute (B-012)
|
|
57
|
+
if (cur == null) clearProp(elm, key, true);
|
|
58
|
+
// G-553: on create (no old sel) `value` is always written: the create hooks run before the
|
|
59
|
+
// children are appended, so an <option>'s value reads '' (no text yet) and value="" would be
|
|
60
|
+
// skipped, leaving no attribute (the option then submits its label)
|
|
61
|
+
else if (key !== 'value' || elm[key] !== cur || !oldVnode.sel) elm[key] = cur;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const propsModule = {create: updateProps, update: updateProps};
|
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Snabbdom module that
|
|
2
|
+
* Snabbdom module that applies a <select>'s `value` after its <option>s exist.
|
|
3
3
|
*
|
|
4
|
-
* Problem: snabbdom
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Problem: snabbdom runs the module create/update hooks (propsModule sets
|
|
5
|
+
* elm.value) BEFORE it creates or patches the element's children. When the
|
|
6
|
+
* matching <option> is new in this patch, the browser ignores the value and
|
|
7
|
+
* the <select> shows the first option. On create that is every <select>; on
|
|
8
|
+
* update it is a value change together with an options change (B-017).
|
|
8
9
|
*
|
|
9
|
-
* Fix: collect <select> elements
|
|
10
|
-
* then re-apply the value in the post hook
|
|
11
|
-
* children have been inserted).
|
|
10
|
+
* Fix: collect <select> elements with a value prop during create and update,
|
|
11
|
+
* then re-apply the value in the post hook, after all children are patched.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type {VNode} from 'snabbdom/build/vnode.js';
|
|
15
|
+
import {isField} from './controlledInputModule';
|
|
15
16
|
|
|
16
17
|
let pendingSelects: Array<{elm: HTMLSelectElement; value: any}> = [];
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
19
|
+
function queueSelect(_oldVnode: VNode, vnode: VNode): void {
|
|
20
|
+
// P45-A: a form field by the vnode's tag first; most vnodes aren't
|
|
21
|
+
if (!isField(vnode)) return;
|
|
22
|
+
const elm = vnode.elm as HTMLSelectElement;
|
|
23
|
+
const value = vnode.data?.props?.value;
|
|
24
|
+
if (elm && elm.tagName === 'SELECT' && value != null) {
|
|
25
|
+
pendingSelects.push({elm, value});
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
function postPatch(): void {
|
|
26
|
-
|
|
27
|
-
const {elm, value} = pendingSelects[i];
|
|
28
|
-
elm.value = value;
|
|
29
|
-
}
|
|
30
|
+
const list = pendingSelects;
|
|
30
31
|
pendingSelects = [];
|
|
32
|
+
for (let i = 0; i < list.length; i++) {
|
|
33
|
+
const {elm, value} = list[i];
|
|
34
|
+
if (elm.value !== String(value)) elm.value = value;
|
|
35
|
+
}
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
export const selectModule = {create:
|
|
38
|
+
export const selectModule = {create: queueSelect, update: queueSelect, post: postPatch};
|
|
@@ -13,14 +13,9 @@
|
|
|
13
13
|
export {h, fragment as HFragment} from 'snabbdom/build/h.js';
|
|
14
14
|
export {init} from 'snabbdom/build/init.js';
|
|
15
15
|
export type {Options} from 'snabbdom/build/init.js';
|
|
16
|
-
export {toVNode} from 'snabbdom/build/tovnode.js';
|
|
17
16
|
export {vnode} from 'snabbdom/build/vnode.js';
|
|
18
17
|
export type {VNode, VNodeData} from 'snabbdom/build/vnode.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Tag Fragment so we can identify it even after minification mangles Function.name
|
|
22
|
-
(_Fragment as any).__sygnalFragment = true;
|
|
23
|
-
export const Fragment = _Fragment;
|
|
18
|
+
export {Fragment} from './fragment';
|
|
24
19
|
|
|
25
20
|
// Module type
|
|
26
21
|
export type {Module} from 'snabbdom/build/modules/module.js';
|
package/src/cycle/dom/thunk.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {h, VNode, VNodeData} from './snabbdom';
|
|
2
|
+
import {flat} from './utils';
|
|
2
3
|
|
|
3
4
|
export interface ThunkData extends VNodeData {
|
|
4
5
|
fn(): VNode;
|
|
@@ -10,6 +11,8 @@ export interface Thunk extends VNode {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function copyToThunk(vnode: VNode, thunkVNode: Thunk): void {
|
|
14
|
+
// (rendered during the patch: its fragments are flattened here, as the DOM driver does)
|
|
15
|
+
vnode = flat(vnode);
|
|
13
16
|
thunkVNode.elm = vnode.elm;
|
|
14
17
|
(vnode.data as ThunkData).fn = thunkVNode.data.fn;
|
|
15
18
|
(vnode.data as ThunkData).args = thunkVNode.data.args;
|
package/src/cycle/dom/utils.ts
CHANGED
|
@@ -86,3 +86,101 @@ export function makeInsert(
|
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
// G-261: a DOM change Sygnal makes outside a patch (a Transition's leave, a Portal mounted late)
|
|
91
|
+
// re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root)
|
|
92
|
+
export const POKE = 'sygnal-dom';
|
|
93
|
+
export const pokeDOM = (el: any): any => el?.dispatchEvent(new Event(POKE, {bubbles: true}));
|
|
94
|
+
|
|
95
|
+
const obj = (o: any) => o !== null && typeof o == 'object' && !Array.isArray(o);
|
|
96
|
+
/**
|
|
97
|
+
* P46-P: two vnodes' data are the same for the DOM modules: the same keys, each value identical
|
|
98
|
+
* or a shallow-equal object (a module bucket: props, attrs, class, dataset, style, ...)
|
|
99
|
+
*/
|
|
100
|
+
export function sameData(a: any, b: any): boolean {
|
|
101
|
+
if (a === b) return true;
|
|
102
|
+
if (!a || !b) return false;
|
|
103
|
+
let n = 0;
|
|
104
|
+
for (const k in a) {
|
|
105
|
+
n++;
|
|
106
|
+
const x = a[k], y = b[k];
|
|
107
|
+
if (x !== y) {
|
|
108
|
+
if (!obj(x) || !obj(y)) return false;
|
|
109
|
+
let m = 0;
|
|
110
|
+
for (const j in x) { m++; if (x[j] !== y[j]) return false; }
|
|
111
|
+
for (const _ in y) m--;
|
|
112
|
+
if (m) return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const _ in b) n--;
|
|
116
|
+
return !n;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// G-517 / G-518 / G-522: snabbdom never sees a fragment. Before each patch, a fragment's children
|
|
120
|
+
// (through nested ones) are spliced into its parent's children, so they are diffed, moved and
|
|
121
|
+
// adopted at hydration as the parent's own (snabbdom's fragment bounds went stale: an insert into
|
|
122
|
+
// one landed at the parent's end, a keyed one never moved, a later patch threw NotFoundError).
|
|
123
|
+
// The children of a keyed fragment (a Collection item, a component's root) take its key and their
|
|
124
|
+
// own key, else their tag and its count among them: they move with it and stay apart from another
|
|
125
|
+
// item's. G-541: no two paths give the same key (a '/' separator did: fragment "src" + "a/b.ts" and
|
|
126
|
+
// fragment "src/a" + "b.ts"). 3-W G-544: each segment is a JSON string (the key as a string) or
|
|
127
|
+
// array ([tag, count], always last), which ends where it ends, appended to the prefix
|
|
128
|
+
// (`"src""a/b.ts"`): a key grows linearly with the nesting (JSON in JSON doubled at each level),
|
|
129
|
+
// and any key String() takes is fine (G-548: a BigInt; two Symbols with the same description share
|
|
130
|
+
// a key; a number key and its string are one key, as in snabbdom). A vnode with no fragment below is kept by identity, as is one already
|
|
131
|
+
// flattened (a cached subtree), so snabbdom still skips unchanged subtrees; so are a keyed
|
|
132
|
+
// fragment's copies while the fragment and its prefix are the same (G-539; a hit compares the
|
|
133
|
+
// parent's prefix, the fragment's own is made on a miss). A hoisted keyed fragment rendered in two
|
|
134
|
+
// places shares its copies, as any hoisted vnode shares its object (render it once). A copy (a
|
|
135
|
+
// parent with new children, a child with a new key) has its vnode's fields and writes its
|
|
136
|
+
// element there too, so the app's vnodes get their `elm` as before (testing and diagnostics read
|
|
137
|
+
// it); it keeps its own (a vnode object reused under another key: the old copy removes its own).
|
|
138
|
+
const flatOf = new WeakMap<any, any>();
|
|
139
|
+
const cpOf = new WeakMap<any, any>();
|
|
140
|
+
// 4-I G-561: a copy is an instance of one class (the vnode's own fields, `elm` an accessor on its
|
|
141
|
+
// prototype), not Object.create(y) with an own accessor: snabbdom's patch loop read each field
|
|
142
|
+
// through the prototype chain and called a per-object accessor, ~3x slower over a 1k Collection.
|
|
143
|
+
// (y: set again after the fields, as a copy's own y is its vnode's vnode; m starts as the vnode's
|
|
144
|
+
// elm, which snabbdom replaces before reading it)
|
|
145
|
+
class Copy {
|
|
146
|
+
y: any; m: any
|
|
147
|
+
constructor(y: any, o: any) { this.y = y; Object.assign(this, y, o).y = y }
|
|
148
|
+
get elm() { return this.m }
|
|
149
|
+
set elm(e: any) { this.y.elm = this.m = e }
|
|
150
|
+
}
|
|
151
|
+
const seg = (k: any, s: any) => (k || '') + JSON.stringify(typeof s == 'object' ? s : String(s));
|
|
152
|
+
export const flat = (v: any): any => {
|
|
153
|
+
if (!v || v.$p || !v.children) return v;
|
|
154
|
+
let r = flatOf.get(v);
|
|
155
|
+
if (!r) {
|
|
156
|
+
const o: any[] = [];
|
|
157
|
+
let d = 0;
|
|
158
|
+
// (m, s: locals of each call)
|
|
159
|
+
const put = (c: any[], k?: string, n: any = {}, m?: any, s?: any) => {
|
|
160
|
+
for (const x of c) {
|
|
161
|
+
// G-539: a keyed fragment's copies are kept per (fragment, prefix), in flatOf (flat() never
|
|
162
|
+
// takes a fragment): an unchanged one (a Collection item that didn't render again) gives
|
|
163
|
+
// snabbdom the same vnodes
|
|
164
|
+
if (x && !x.sel && x.children) d = 1, x.key == null ? put(x.children, k, n)
|
|
165
|
+
: (m = flatOf.get(x)) && m[0] === k ? o.push(...m[1])
|
|
166
|
+
: (s = o.length, put(x.children, seg(k, x.key)), flatOf.set(x, [k, o.slice(s)]));
|
|
167
|
+
else {
|
|
168
|
+
let y = flat(x);
|
|
169
|
+
y !== x && (d = 1);
|
|
170
|
+
if (k && y) {
|
|
171
|
+
// 4-H: the copy of an unchanged child under the same key is kept (the fragment is new
|
|
172
|
+
// when one of its children is: a Collection's), so snabbdom skips the others. 4-I
|
|
173
|
+
// G-561: a hit compares the prefix and the child's own part (its key, else its count),
|
|
174
|
+
// so the key is only built on a miss
|
|
175
|
+
const s = y.key ?? (n[y.sel] = -~n[y.sel]), c = cpOf.get(y);
|
|
176
|
+
y = c && c[0] === k && c[1] === s ? c[2] : (cpOf.set(y, [k, s, y = new Copy(y, {key: seg(k, y.key ?? [y.sel, s])})]), y);
|
|
177
|
+
}
|
|
178
|
+
o.push(y);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
put(v.children);
|
|
183
|
+
flatOf.set(v, r = d ? new Copy(v, {children: o}) : v);
|
|
184
|
+
}
|
|
185
|
+
return r;
|
|
186
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import xs, {Stream} from 'xstream';
|
|
2
|
+
import type {VNode} from './snabbdom';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PLAN-4 GS-12 (D129): the patch that something asked for runs inside
|
|
6
|
+
* `document.startViewTransition()`. The ask is `flags().vt`, set by the core when a component's
|
|
7
|
+
* `viewTransitions` action changes state (on the app's IsolateModule, so it is per app).
|
|
8
|
+
*
|
|
9
|
+
* - No API, or `prefers-reduced-motion: reduce`, or the first render: patched at once.
|
|
10
|
+
* - P45-C: one action is one patch (the app's render scheduler flushes every component, a
|
|
11
|
+
* Collection move included, into one), so the update callback patches with the latest vnode
|
|
12
|
+
* that arrived before the browser called back, and is done once that patch is (no quiet
|
|
13
|
+
* window: it was 20 ms, capped at 200 ms, for the several patches of a move).
|
|
14
|
+
* - A new transition while one animates: the browser skips the running one (it jumps to its end).
|
|
15
|
+
* - G-489: a skipped transition's `ready` rejection is handled (no uncaught InvalidStateError).
|
|
16
|
+
*/
|
|
17
|
+
export function viewTransition$(vnode$: Stream<VNode>, flags: () => any): Stream<VNode> {
|
|
18
|
+
const g: any = globalThis, out$ = xs.create<VNode>();
|
|
19
|
+
let held: VNode[] | 0 = 0, first = 1;
|
|
20
|
+
return xs.merge(vnode$.filter(v => {
|
|
21
|
+
const f = flags() || {}, d = g.document, want = f.vt && !first;
|
|
22
|
+
// the first render has no old view to transition from (a ROUTE reply at start asks too)
|
|
23
|
+
f.vt = first = 0;
|
|
24
|
+
if (held) return held[0] = v, false;
|
|
25
|
+
if (!want || !d?.startViewTransition || g.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return true;
|
|
26
|
+
held = [v];
|
|
27
|
+
const update = () => {
|
|
28
|
+
const h = held as VNode[];
|
|
29
|
+
held = 0;
|
|
30
|
+
out$.shamefullySendNext(h[0]);
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
// G-489: a transition the browser skips (duplicate names, a newer one) rejects `ready`:
|
|
34
|
+
// handled (the update still runs; `finished` / `updateCallbackDone` reject only when it throws).
|
|
35
|
+
// (a stand-in without `ready` must not throw here: the update would run twice)
|
|
36
|
+
d.startViewTransition(update)?.ready?.catch(Object);
|
|
37
|
+
} catch (_) {
|
|
38
|
+
// the browser refused (e.g. an invalid state): patch now
|
|
39
|
+
update();
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}), out$);
|
|
43
|
+
}
|
|
@@ -3,6 +3,7 @@ import {dropRepeats} from '../../extra/xstreamExtras';
|
|
|
3
3
|
import {DevToolEnabledSource} from '../run/types';
|
|
4
4
|
import {adapt} from '../run/adapt';
|
|
5
5
|
import {Getter, Setter, Scope, Reducer} from './types';
|
|
6
|
+
import {objIsEqual} from './objIsEqual';
|
|
6
7
|
|
|
7
8
|
function updateArrayEntry<T>(
|
|
8
9
|
array: Array<T>,
|
|
@@ -87,9 +88,13 @@ export class StateSource<S> {
|
|
|
87
88
|
public stream: MemoryStream<S>;
|
|
88
89
|
private _stream: MemoryStream<S>;
|
|
89
90
|
private _name: string;
|
|
91
|
+
private _end?: Stream<any>;
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
// raw (P45-D): `stream` is another StateSource's stream mapped, already without undefined and
|
|
94
|
+
// repeats (a component's own source), so one stream instead of four
|
|
95
|
+
constructor(stream: Stream<S>, name: string, end?: Stream<any>, raw?: any) {
|
|
96
|
+
this._end = end;
|
|
97
|
+
this._stream = raw ? stream.remember() : stream
|
|
93
98
|
.filter(s => typeof s !== 'undefined')
|
|
94
99
|
.compose(dropRepeats())
|
|
95
100
|
.remember();
|
|
@@ -105,7 +110,18 @@ export class StateSource<S> {
|
|
|
105
110
|
*/
|
|
106
111
|
public select<R>(scope: Scope<S, R>): StateSource<R> {
|
|
107
112
|
const get = makeGetter(scope);
|
|
108
|
-
|
|
113
|
+
// G-214: keeps the end stream, so a selected source's watch() also ends on dispose
|
|
114
|
+
return new StateSource<R>(this._stream.map(get) as Stream<R>, this._name, this._end);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* PLAN-4 GS-6: selector(state) whenever it changes structurally (objIsEqual). The current
|
|
119
|
+
* value is the baseline, not a change, unless { immediate: true }. Ends with the state stream
|
|
120
|
+
* or, for a component's own STATE source, when the component is disposed
|
|
121
|
+
*/
|
|
122
|
+
public watch<R>(selector: (state: S) => R, {immediate}: {immediate?: boolean} = {}): Stream<R> {
|
|
123
|
+
const s$ = this._stream.map(selector).compose(dropRepeats(objIsEqual)).endWhen(this._end || xs.never());
|
|
124
|
+
return immediate ? s$ : s$.drop(1);
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
public isolateSource = isolateSource;
|