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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {makeReplies} from './replies';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* makeTimerDriver() (PLAN-4 GS-7, D114): timers declared per component instance from state,
|
|
5
|
+
* answered with reply actions (./replies.ts). Registered: `run(App, { TIMER: makeTimerDriver() })`.
|
|
6
|
+
* The core has no timer code (0 B): it sends a component's `timers` static to the source marked
|
|
7
|
+
* `__sygnalStatic: 'timers'` (initStatics), stamped with the sender (`__emitterId`).
|
|
8
|
+
*
|
|
9
|
+
* - Declaration: `C.timers = (state) => ({ [name]: spec | falsy })`, spec one of
|
|
10
|
+
* `{ every: ms, action }`, `{ after: ms, action }`, `{ frame: 'ACTION' }`, each with an
|
|
11
|
+
* optional `background: true`.
|
|
12
|
+
* - Diffed per (sender, name), comparing specs structurally: a new name starts, a removed or
|
|
13
|
+
* falsy one stops, a changed spec restarts (an equal one keeps running).
|
|
14
|
+
* - `every`: drift-free; tick n is due at start + n * every (each delay is computed from the
|
|
15
|
+
* start, not chained). A late tick (a throttled background tab) coalesces the missed ones:
|
|
16
|
+
* n jumps. Data `{ n, t }`: the tick number from 1, `t` = Date.now().
|
|
17
|
+
* - `after`: fires once, data `{ t }`. It doesn't fire again while the same spec stays declared.
|
|
18
|
+
* - `frame`: every animation frame (requestAnimationFrame; setTimeout 16 ms without it), data
|
|
19
|
+
* `{ t, dt }`: Date.now() and the ms since the previous frame (0 on the first).
|
|
20
|
+
* - A hidden Switchable page's timers stop unless `background: true` (the core sends only those
|
|
21
|
+
* while hidden) and start again from scratch when it is shown.
|
|
22
|
+
* - A disposed instance's timers stop; app dispose stops all. SSR runs no drivers: no timers.
|
|
23
|
+
* - An invalid spec (non-positive `every`, negative `after`, no action) doesn't start; the dev
|
|
24
|
+
* entry reports SYG422 through the `timerSpec` diagnostics hook.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const g: any = globalThis;
|
|
28
|
+
const now = () => Date.now();
|
|
29
|
+
|
|
30
|
+
const valid = (s: any) => s.frame
|
|
31
|
+
? typeof s.frame == 'string'
|
|
32
|
+
: typeof s.action == 'string' && !!s.action && (s.every == null ? s.after >= 0 : s.after == null && s.every > 0) && (s.every || s.after) < 1 / 0;
|
|
33
|
+
|
|
34
|
+
// starts one timer; `e` (its entry) gets `d` once an `after` has fired. Returns the stop function.
|
|
35
|
+
// The next tick / frame is scheduled before the action is sent, so an action that stops (or
|
|
36
|
+
// changes) the timer clears the pending one.
|
|
37
|
+
const start = (s: any, send: (type: string, data: any) => void, e: any): (() => void) => {
|
|
38
|
+
let id: any, n = 0, last: any;
|
|
39
|
+
const t0 = now(), every = s.every, raf = s.frame && g.requestAnimationFrame;
|
|
40
|
+
const later = (ms: number) => id = raf ? requestAnimationFrame(tick) : setTimeout(tick, ms);
|
|
41
|
+
// a frame's `ts` is requestAnimationFrame's timestamp (the fallback passes none: the clock)
|
|
42
|
+
const tick = (ts = now()) => {
|
|
43
|
+
const t = now();
|
|
44
|
+
if (s.frame) {
|
|
45
|
+
later(16);
|
|
46
|
+
send(s.frame, {t, dt: last == null ? 0 : ts - last});
|
|
47
|
+
last = ts;
|
|
48
|
+
} else if (every) {
|
|
49
|
+
n = Math.max(n + 1, Math.floor((t - t0) / every));
|
|
50
|
+
later(t0 + (n + 1) * every - t);
|
|
51
|
+
send(s.action, {n, t});
|
|
52
|
+
} else {
|
|
53
|
+
e.d = 1;
|
|
54
|
+
send(s.action, {t});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
later(s.frame ? 16 : every || s.after);
|
|
58
|
+
return () => raf ? cancelAnimationFrame(id) : clearTimeout(id);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The driver over `runners` (sender → { c: component name, on: { [name]: entry } }, an entry
|
|
63
|
+
* being { k: the spec's JSON, s: the spec, x: stop, ok: started, d: an `after` that fired }).
|
|
64
|
+
* The testing fake passes its own map to list the active timers (t.timers).
|
|
65
|
+
*/
|
|
66
|
+
export const timerDriver = (runners: Map<any, any>) => (sink$: any) => {
|
|
67
|
+
const update = (id: any, d: any, c?: any) => {
|
|
68
|
+
let r = runners.get(id);
|
|
69
|
+
if (!r) runners.set(id, r = {c, on: {}});
|
|
70
|
+
const on = r.on;
|
|
71
|
+
for (const n in on) if (on[n].k != JSON.stringify(d?.[n])) on[n].x(), delete on[n];
|
|
72
|
+
for (const n in d) {
|
|
73
|
+
const s = d[n];
|
|
74
|
+
if (!s || on[n]) continue;
|
|
75
|
+
const e: any = on[n] = {k: JSON.stringify(s), s, x: () => {}};
|
|
76
|
+
if (e.ok = valid(s)) e.x = start(s, (type, data) => reply(id, type, data), e);
|
|
77
|
+
else g.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
|
|
78
|
+
}
|
|
79
|
+
if (!d) runners.delete(id);
|
|
80
|
+
};
|
|
81
|
+
const stopAll = () => runners.forEach((_, id) => update(id, 0));
|
|
82
|
+
const {replies, reply} = makeReplies(id => update(id, 0));
|
|
83
|
+
sink$.addListener({
|
|
84
|
+
next: (v: any) => { if (v && v.__emitterId !== undefined && 'timers' in v) update(v.__emitterId, v.timers, v.__emitterName); },
|
|
85
|
+
error: () => {},
|
|
86
|
+
complete: stopAll,
|
|
87
|
+
});
|
|
88
|
+
return {...replies, __sygnalStatic: 'timers', dispose: stopAll};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Timers declared by components (`C.timers = (state) => ({ tick: state.running && { every: 100,
|
|
93
|
+
* action: 'TICK' } })`), delivered as their own actions: `run(App, { TIMER: makeTimerDriver() })`.
|
|
94
|
+
*/
|
|
95
|
+
export const makeTimerDriver = () => timerDriver(new Map());
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-4 GS-8: undo / redo.
|
|
3
|
+
*
|
|
4
|
+
* Editor.model = undoable({ TYPE: ..., LOAD: ... }, { key: 'doc', coalesceMs: 500, resetOn: ['LOAD'] })
|
|
5
|
+
* → state.history = { past, future }; adds UNDO and REDO (the host's intent triggers them)
|
|
6
|
+
* Editor.uses = { history: undo({ key: 'doc', undo: UndoButton, redo: RedoButton }) }
|
|
7
|
+
* → the same as a behavior: state.history also has canUndo / canRedo, the actions are
|
|
8
|
+
* 'history.UNDO' / 'history.REDO', and the controls trigger them
|
|
9
|
+
*
|
|
10
|
+
* Every STATE reducer (function entries, `STATE` of object entries, 'ACTION | STATE' keys) is
|
|
11
|
+
* wrapped: when its result has a different state[key], the old state[key] is pushed onto
|
|
12
|
+
* `past` (at most `limit`, default 100) and `future` is cleared. With `track`, only those
|
|
13
|
+
* actions are recorded; built-in actions (INITIALIZE ...) only when tracked. A change by the
|
|
14
|
+
* same action within `coalesceMs` of the previous one joins that step (typing). With
|
|
15
|
+
* `coalesce` (4-G1, D143) only the listed actions join (`coalesceMs` then defaults to 500);
|
|
16
|
+
* every other action is always its own step (two quick clicks on Larger are two). `resetOn`
|
|
17
|
+
* actions clear the history and are not recorded. Snapshots are references: reducers must
|
|
18
|
+
* return new objects (they do in Sygnal). A model's own UNDO / REDO entry runs after the
|
|
19
|
+
* built-in one (as a host entry for a behavior action does, D123).
|
|
20
|
+
*
|
|
21
|
+
* Gestures (3-H G-447): another behavior in the host's `uses` that declares `undoStep` (its
|
|
22
|
+
* actions that complete a step: sortable's DROPPED) makes its other actions gesture steps. Their
|
|
23
|
+
* changes aren't recorded; `history.base` holds the value from before the gesture until the
|
|
24
|
+
* completing action records it as one entry. Works in either `uses` order (undo first: the
|
|
25
|
+
* behavior's actions get host entries that run after them). undo() as a behavior only:
|
|
26
|
+
* undoable() can't see `uses`. 3-L: a recorded action, UNDO or REDO during a gesture first
|
|
27
|
+
* records the value from before it (G-473: it stays reachable); a gesture step that brings the
|
|
28
|
+
* value back to where the gesture started (a cancel) leaves nothing pending, nor does a gesture
|
|
29
|
+
* action after the value was changed outside the gesture (G-475 / G-479). `track` / `coalesce`
|
|
30
|
+
* naming any of a gesture behavior's actions apply to its steps; `resetOn` naming one of them
|
|
31
|
+
* resets the history at that action (G-477). 3-O: an untracked gesture is like an untracked
|
|
32
|
+
* action, its changes are never recorded, nor pending (G-506); drops join only when `coalesce`
|
|
33
|
+
* names a gesture action (G-507); REDO mid-gesture records the value from before the gesture in
|
|
34
|
+
* place of the current one, as a recorded action does (G-508); "back where it started" compares
|
|
35
|
+
* arrays and plain objects shallowly (G-509). 3-U: with `key: [...]` every comparison is key by key
|
|
36
|
+
* (G-529 / G-530), a key the state didn't have stays out on UNDO / REDO, and `key: []` throws (G-531).
|
|
37
|
+
*
|
|
38
|
+
* SYG226 (dev, warn): a `track` / `resetOn` / `coalesce` name with no model entry (nor an action
|
|
39
|
+
* of a behavior in the host's `uses`).
|
|
40
|
+
*/
|
|
41
|
+
import { defineBehavior } from './behaviors'
|
|
42
|
+
import { ABORT, isAbort } from '../shared'
|
|
43
|
+
|
|
44
|
+
const BUILT_IN = /^(BOOTSTRAP|INITIALIZE|DISPOSE|READY|RESOURCE)$/
|
|
45
|
+
// the last recorded change of a history, keyed by its `past` array (which a behavior's calculated
|
|
46
|
+
// fields keep while they copy the history object): [action, time] (coalescing; never in state)
|
|
47
|
+
const last = new WeakMap<object, [string, number]>()
|
|
48
|
+
const reported = new WeakSet<object>()
|
|
49
|
+
|
|
50
|
+
export interface UndoOptions { key: string | string[], limit?: number, track?: string[], coalesce?: string[], coalesceMs?: number, resetOn?: string[] }
|
|
51
|
+
|
|
52
|
+
const actionOf = (a: string) => a.split('|')[0].trim()
|
|
53
|
+
|
|
54
|
+
// 3-U G-531: `key: []` keeps nothing (a definition error, in every build)
|
|
55
|
+
const keyed = (o: UndoOptions) => {
|
|
56
|
+
if (Array.isArray(o?.key) && !o.key.length) throw new Error("undo: `key: []` names no state key; use key: 'name' or key: ['a', 'b']")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// SYG226: report through the diagnostics core when it is enabled (dev); nothing in production
|
|
60
|
+
const check = (model: any, o: UndoOptions, component?: any, skip?: any) => {
|
|
61
|
+
const known = new Set(Object.keys(model || {}).map(actionOf))
|
|
62
|
+
for (const a of [...(o.track || []), ...(o.resetOn || []), ...(o.coalesce || [])]) {
|
|
63
|
+
const i = a.indexOf('.'), m = i > 0 && skip?.[a.slice(0, i)]?.model
|
|
64
|
+
// a behavior's action (G-477: one it doesn't have is reported)
|
|
65
|
+
if (known.has(a) || m && a.slice(i + 1) in m) continue
|
|
66
|
+
try {
|
|
67
|
+
(globalThis as any).__SYGNAL_DIAGNOSTICS__?.report('SYG226', {
|
|
68
|
+
severity: 'warn', component, data: {action: a},
|
|
69
|
+
message: `undo ${o.track?.includes(a) ? 'track' : o.resetOn?.includes(a) ? 'resetOn' : 'coalesce'} names '${a}', which has no model entry`,
|
|
70
|
+
fix: `Use the name of a model entry, or add '${a}' to the model`,
|
|
71
|
+
})
|
|
72
|
+
} catch (e) { queueMicrotask(() => { throw e }) }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// a history without the pending gesture base
|
|
77
|
+
const settled = (h: any) => { const {base: _, ...o} = h; return o }
|
|
78
|
+
// the same value (a gesture step that put everything back: a cancel): arrays and plain objects
|
|
79
|
+
// compare their entries by identity (G-509), anything else is itself
|
|
80
|
+
const plain = (x: any) => Array.isArray(x) ? 1 : x && typeof x == 'object' && Object.getPrototypeOf(x) === Object.prototype ? 2 : 0
|
|
81
|
+
const same1 = (a: any, b: any) => {
|
|
82
|
+
const k = a !== b && plain(a) && plain(a) == plain(b) && Object.keys(a)
|
|
83
|
+
return a === b || !!k && k.length == Object.keys(b).length && k.every(i => i in b && a[i] === b[i])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const wrap = (model: any, o: UndoOptions, hk: string, ns: string, S = 'STATE', g: any = {}): any => {
|
|
87
|
+
const {key, limit = 100, track, coalesce, coalesceMs = coalesce ? 500 : 0, resetOn = []} = o, keys = typeof key == 'string' ? null : key
|
|
88
|
+
const out: any = {}
|
|
89
|
+
const hist = (s: any) => s?.[hk] || {past: [], future: []}
|
|
90
|
+
// the value undo keeps: state[key], or (3-P G-515: `key: ['todo', 'done']`) an object of those
|
|
91
|
+
// keys the state has; values compare key by key (3-U G-529 / G-530: no shared identity, no cache).
|
|
92
|
+
// 3-W G-536: a key the state lacks and one it has as `undefined` differ for an array key (a key
|
|
93
|
+
// that appears or goes is a change); for a string key both are the value `undefined`. Primitive
|
|
94
|
+
// state (before the first object) has none of the keys
|
|
95
|
+
const val = (s: any) => {
|
|
96
|
+
if (!keys) return s?.[key as string]
|
|
97
|
+
const v: any = {}
|
|
98
|
+
for (const k of keys) if (s && typeof s == 'object' && k in s) v[k] = s[k]
|
|
99
|
+
return v
|
|
100
|
+
}
|
|
101
|
+
const by = (a: any, b: any, f: any) => !!a && !!b && keys!.every(k => k in a == k in b && f(a[k], b[k]))
|
|
102
|
+
const eq = (a: any, b: any) => keys ? a === b || by(a, b, (x: any, y: any) => x === y) : a === b
|
|
103
|
+
const same = (a: any, b: any) => keys ? by(a, b, same1) : same1(a, b)
|
|
104
|
+
// G-531: a key the snapshot didn't have is left out (not set to undefined); G-536: so is a string
|
|
105
|
+
// key whose snapshot is undefined
|
|
106
|
+
const set = (s: any, v: any) => {
|
|
107
|
+
const r = keys ? {...s, ...v} : {...s, [key as string]: v}
|
|
108
|
+
for (const k of keys || [key as string]) if (keys ? !(k in v) : v === undefined) delete r[k]
|
|
109
|
+
return r
|
|
110
|
+
}
|
|
111
|
+
// one more entry in `past` (`join`: the change joins the last one)
|
|
112
|
+
const rec = (r: any, h: any, v: any, name: string, joins: any) => {
|
|
113
|
+
const at = Date.now(), prev = last.get(h.past)
|
|
114
|
+
const join = joins && prev && prev[0] == name && at - prev[1] < coalesceMs && h.past.length
|
|
115
|
+
const nh = {...settled(h), past: join ? h.past : [...h.past, v].slice(-limit), future: []}
|
|
116
|
+
last.set(nh.past, [name, at])
|
|
117
|
+
return {...r, [hk]: nh}
|
|
118
|
+
}
|
|
119
|
+
// 3-H G-447: a gesture's actions (`g`: another behavior's actions, 1 a step, 2 the one that
|
|
120
|
+
// completes it, its `undoStep`). A step's change isn't recorded: `base` keeps [the value before
|
|
121
|
+
// the gesture's first step, the value after its last]; the completing action records that
|
|
122
|
+
// first value as one entry. A gesture that ends without it (cancelled) records nothing. The
|
|
123
|
+
// pre-action state comes from the handler's props (`state`): as the host entry after the
|
|
124
|
+
// behavior's (undo before it in `uses`), the reducer gets the behavior's result. `all`: the
|
|
125
|
+
// gesture behavior's actions (G-477: `track` / `coalesce` naming any of them cover its steps);
|
|
126
|
+
// `n` its key in `uses`, `end` its internal `undoEnd(slice)`: true when the slice an action
|
|
127
|
+
// left ended the gesture without completing it (3-P G-510: sortable's Escape, unmount, reset)
|
|
128
|
+
const gw = (f: any, [kind, all, n, end]: any, name: string) => {
|
|
129
|
+
const has = (l?: string[]) => !!l && all.some((x: string) => l.includes(x))
|
|
130
|
+
// G-507: drops join only when `coalesce` names the gesture (two separate drags are two steps)
|
|
131
|
+
const tracked = !track || has(track), joins = coalesceMs > 0 && has(coalesce), reset = resetOn.includes(name)
|
|
132
|
+
return (s: any, ...x: any[]) => {
|
|
133
|
+
const r0 = f ? f(s, ...x) : s, r = isAbort(r0) ? s : r0, pre = x[2]?.state || s, none = f ? r0 : ABORT
|
|
134
|
+
if (!r || typeof r != 'object' || !pre) return none
|
|
135
|
+
const h = hist(r), b = h.base, v = val(r), p = val(pre)
|
|
136
|
+
// G-477: resetOn names this gesture action
|
|
137
|
+
if (reset) return h.past.length || h.future.length || b ? {...r, [hk]: {...settled(h), past: [], future: []}} : none
|
|
138
|
+
// G-506: an untracked gesture is an untracked change: nothing recorded, nothing pending
|
|
139
|
+
if (!tracked) return none
|
|
140
|
+
// the base holds while the value is the one the gesture's last step left (G-475: else it
|
|
141
|
+
// was changed outside the gesture, e.g. restored, and the base is stale)
|
|
142
|
+
const on = b && eq(b[1], p) && b, stop = kind == 1 && end?.(r[n])
|
|
143
|
+
if (kind == 1 && !stop) {
|
|
144
|
+
if (eq(v, p)) return b && !on ? {...r, [hk]: settled(h)} : none
|
|
145
|
+
const b0 = on ? on[0] : p
|
|
146
|
+
// back where the gesture started (Escape, an item moved back): nothing pending (G-479);
|
|
147
|
+
// the base is this behavior's (G-514: a step joins another's pending base and takes it)
|
|
148
|
+
return {...r, [hk]: same(b0, v) ? settled(h) : {...h, base: [b0, v, n]}}
|
|
149
|
+
}
|
|
150
|
+
// G-514: only the behavior that opened the base completes it
|
|
151
|
+
if (b && b[2] != null && b[2] !== n || !b && !stop) return none
|
|
152
|
+
// G-510: ended without completing: what it leaves (the item where it is, or a restore after
|
|
153
|
+
// the value changed) is one step from the value before the gesture, unless it is that value
|
|
154
|
+
if (stop) {
|
|
155
|
+
const b0 = on ? on[0] : p
|
|
156
|
+
return same(b0, v) ? b ? {...r, [hk]: settled(h)} : none : rec(r, h, b0, name, 0)
|
|
157
|
+
}
|
|
158
|
+
return !on || eq(b[0], v) ? {...r, [hk]: settled(h)} : rec(r, h, b[0], name, joins)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// G-473: a pending gesture's base, recorded before another change: the value from before the
|
|
162
|
+
// gesture stays reachable (its steps join that change's entry)
|
|
163
|
+
const before = (h: any, v: any) => { const b = h.base; return b && eq(b[1], v) && !eq(b[0], v) ? [b[0]] : null }
|
|
164
|
+
for (const a in g) if (!(a in model)) out[a] = {[S]: gw(null, g[a], a)}
|
|
165
|
+
for (const a in model) {
|
|
166
|
+
const e = model[a], [name, sink] = a.split('|').map(x => x.trim())
|
|
167
|
+
if (g[a]) {
|
|
168
|
+
const f = typeof e == 'function' ? e : e?.[S]
|
|
169
|
+
out[a] = typeof e == 'function' ? gw(e, g[a], a) : {...e, [S]: gw(typeof f == 'function' ? f : null, g[a], a)}
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
const reset = resetOn.includes(name), joins = coalesceMs > 0 && (!coalesce || coalesce.includes(name))
|
|
173
|
+
const f0 = sink ? (sink == S ? e : null) : typeof e == 'function' ? e : e?.[S]
|
|
174
|
+
// a constant STATE value isn't a reducer: the entry is left alone (G-214)
|
|
175
|
+
if (f0 != null && typeof f0 != 'function') { out[a] = e; continue }
|
|
176
|
+
const f = f0
|
|
177
|
+
// an entry without a STATE reducer is left alone, except a resetOn object entry (gets one)
|
|
178
|
+
if (!(f || reset && !sink) || !(reset || (track ? track.includes(name) : !BUILT_IN.test(name)))) { out[a] = e; continue }
|
|
179
|
+
const w = (s: any, ...x: any[]) => {
|
|
180
|
+
const r = f ? f(s, ...x) : s, h = hist(s)
|
|
181
|
+
if (!r || typeof r != 'object' || isAbort(r)) return r
|
|
182
|
+
if (reset) return h.past.length || h.future.length || h.base ? {...r, [hk]: {...settled(h), past: [], future: []}} : r
|
|
183
|
+
const v = val(s)
|
|
184
|
+
if (r === s || eq(val(r), v)) return r
|
|
185
|
+
const b0 = before(h, v)
|
|
186
|
+
return rec(r, h, b0 ? b0[0] : v, name, !b0 && joins)
|
|
187
|
+
}
|
|
188
|
+
out[a] = sink || typeof e == 'function' ? w : {...e, [S]: w}
|
|
189
|
+
}
|
|
190
|
+
// mid-gesture, the value from before the gesture stands in for the current one: UNDO goes back
|
|
191
|
+
// to it (G-473: the drag so far is the step undone), REDO records it (G-508: the drag so far
|
|
192
|
+
// joins the redone step, as with a recorded action; at most `limit`)
|
|
193
|
+
const step = (u: boolean) => (s: any) => {
|
|
194
|
+
const h = hist(s), v = val(s), b = before(h, v), {past, future} = h
|
|
195
|
+
if (!(u ? b || past.length : future.length)) return ABORT
|
|
196
|
+
return {...set(s, u ? b ? b[0] : past[past.length - 1] : future[0]), [hk]: {...settled(h),
|
|
197
|
+
past: u ? b ? past : past.slice(0, -1) : [...past, b ? b[0] : v].slice(-limit),
|
|
198
|
+
future: u ? [v, ...future] : future.slice(1)}}
|
|
199
|
+
}
|
|
200
|
+
const add = (a: string, f: any) => {
|
|
201
|
+
const own = out[ns + a]
|
|
202
|
+
if (!own) return out[ns + a] = f
|
|
203
|
+
const g = typeof own == 'function' ? own : own[S]
|
|
204
|
+
// the model's own entry runs after the built-in step (its ABORT keeps the step)
|
|
205
|
+
const both = typeof g != 'function' ? f : (s: any, ...x: any[]) => {
|
|
206
|
+
const r = f(s, ...x), q = g(isAbort(r) ? s : r, ...x)
|
|
207
|
+
return isAbort(q) ? r : q
|
|
208
|
+
}
|
|
209
|
+
out[ns + a] = typeof own == 'function' ? both : {...own, [S]: both}
|
|
210
|
+
}
|
|
211
|
+
add('UNDO', step(true))
|
|
212
|
+
add('REDO', step(false))
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Wraps a model's STATE reducers so `state[key]` gets undo / redo (GS-8): `state.history =
|
|
218
|
+
* { past, future }`, plus UNDO and REDO entries for the intent to trigger. Options: `key` (the
|
|
219
|
+
* state key to snapshot), `limit` (100), `track` (only these actions), `coalesceMs` (join rapid
|
|
220
|
+
* changes by one action), `coalesce` (only these actions join; default window 500 ms), `resetOn`
|
|
221
|
+
* (actions that clear the history). Returns a new model.
|
|
222
|
+
*/
|
|
223
|
+
export const undoable = (model: any, options: UndoOptions): any => {
|
|
224
|
+
keyed(options)
|
|
225
|
+
check(model, options)
|
|
226
|
+
return wrap(model, options, 'history', '')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* undoable() as a behavior (GS-8 + GS-1): `uses = { history: undo({ key: 'doc', undo: Undo,
|
|
231
|
+
* redo: Redo }) }` gives `state.history = { past, future, canUndo, canRedo }` and the actions
|
|
232
|
+
* 'history.UNDO' / 'history.REDO' (the `undo` / `redo` controls trigger them; so can a host
|
|
233
|
+
* intent action of that name). The other options are undoable()'s; `track` and `resetOn` name
|
|
234
|
+
* the host's actions.
|
|
235
|
+
*/
|
|
236
|
+
export const undo = (options: UndoOptions & { undo?: any, redo?: any }): any => {
|
|
237
|
+
keyed(options)
|
|
238
|
+
const b = defineBehavior({
|
|
239
|
+
initialState: {past: [], future: []},
|
|
240
|
+
intent: ({DOM}: any, {undo, redo}: any) => ({
|
|
241
|
+
...(undo && {UNDO: DOM.click(undo)}),
|
|
242
|
+
...(redo && {REDO: DOM.click(redo)}),
|
|
243
|
+
}),
|
|
244
|
+
// placeholders (no change): they make the actions behavior-owned; the full-state UNDO / REDO
|
|
245
|
+
// that wrap() adds for 'key.UNDO' runs after them, as a host entry would (D123)
|
|
246
|
+
model: {UNDO: () => ABORT, REDO: () => ABORT},
|
|
247
|
+
calculated: {canUndo: (h: any) => h.past.length > 0, canRedo: (h: any) => h.future.length > 0},
|
|
248
|
+
})(options), merge = b.merge
|
|
249
|
+
b.merge = (c: any, k: string) => {
|
|
250
|
+
const u = c.view?.uses, g: any = {}
|
|
251
|
+
if (!reported.has(b)) reported.add(b), check(c.model, options, c, u)
|
|
252
|
+
// the gestures of the host's other behaviors (sortable's drag: one step per drop), in either `uses` order
|
|
253
|
+
for (const n in u) {
|
|
254
|
+
const s = u[n]?.undoStep, all = Object.keys(u[n]?.model || {}).map(a => n + '.' + a)
|
|
255
|
+
if (s) for (const a in u[n].model) g[n + '.' + a] = [s.includes(a) ? 2 : 1, all, n, u[n].undoEnd]
|
|
256
|
+
}
|
|
257
|
+
c.model = wrap(c.model || {}, options, k, k + '.', c.stateSourceName, g)
|
|
258
|
+
merge(c, k)
|
|
259
|
+
}
|
|
260
|
+
return b
|
|
261
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {Stream} from 'xstream';
|
|
2
|
+
import type {VNode} from '../cycle/dom/snabbdom';
|
|
3
|
+
import {makeDOMDriver, DOMDriverOptions} from '../cycle/dom/makeDOMDriver';
|
|
4
|
+
import {viewTransition$} from '../cycle/dom/viewTransition';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PLAN-4 GS-12 (D129): a DOM driver that can patch inside `document.startViewTransition()`.
|
|
8
|
+
* It is `makeDOMDriver(mountPoint, options)`, as run()'s own DOM driver, plus the hook: the patch caused by an action listed in a component's
|
|
9
|
+
* `viewTransitions` static (`Board.viewTransitions = ['MOVE']`, `App.viewTransitions = ['ROUTE']`)
|
|
10
|
+
* runs as a View Transition. Opt-in, so the hook costs nothing in apps that don't import it.
|
|
11
|
+
*
|
|
12
|
+
* run(App, { DOM: makeViewTransitionDOMDriver('#root') })
|
|
13
|
+
*/
|
|
14
|
+
export function makeViewTransitionDOMDriver(mountPoint: string | Element | DocumentFragment = '#root', options: DOMDriverOptions = {}) {
|
|
15
|
+
const inner: any = makeDOMDriver(mountPoint, options);
|
|
16
|
+
return (vnode$: Stream<VNode>, name?: string) => {
|
|
17
|
+
let source: any;
|
|
18
|
+
// the flags live on the driver's IsolateModule (the core sets `vt` there; it is per app),
|
|
19
|
+
// which exists once the inner driver has run: read lazily, at the first vnode
|
|
20
|
+
source = inner(viewTransition$(vnode$, () => source?._isolateModule), name);
|
|
21
|
+
const im = source._isolateModule;
|
|
22
|
+
// SYG645 (dev): this app's DOM driver can do View Transitions
|
|
23
|
+
im.vtDriver = 1;
|
|
24
|
+
// a request is consumed by the next vnode (viewTransition$ resets it), however late: a render
|
|
25
|
+
// may take long. It expires when the page has been idle for 100 ms without one, since a listed
|
|
26
|
+
// action whose new state renders the same view (the first ROUTE reply, say) sends no vnode and
|
|
27
|
+
// must not animate a later, unrelated patch. A slow render keeps the main thread busy, so the
|
|
28
|
+
// expiry timer then runs late: it is re-armed instead (late by more than 30 ms = busy)
|
|
29
|
+
let on = 0, t: any;
|
|
30
|
+
const expire = (due: number): any => t = setTimeout(() => Date.now() - due > 30 ? expire(Date.now() + 100) : on = 0, 100);
|
|
31
|
+
Object.defineProperty(im, 'vt', {
|
|
32
|
+
configurable: true,
|
|
33
|
+
get: () => on,
|
|
34
|
+
set: (v: any) => { clearTimeout(t); (on = v ? 1 : 0) && expire(Date.now() + 100); },
|
|
35
|
+
});
|
|
36
|
+
return source;
|
|
37
|
+
};
|
|
38
|
+
}
|