@rohal12/spindle 0.59.16 → 0.59.17
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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +73 -22
- package/package.json +1 -1
- package/src/components/macros/PassageDisplay.tsx +9 -1
- package/src/components/macros/Repeat.tsx +6 -1
- package/src/components/macros/Timed.tsx +5 -1
- package/src/components/macros/detached-body.tsx +14 -3
- package/src/define-macro.ts +10 -2
- package/src/hooks/use-modal-focus.ts +2 -0
- package/src/hooks/use-story-fields.ts +8 -0
- package/src/structural.ts +46 -13
- package/src/utils/object-path.ts +13 -2
package/dist/pkg/headless.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, u
|
|
|
3
3
|
import { useSyncExternalStore } from "preact/compat";
|
|
4
4
|
import { createStore } from "zustand/vanilla";
|
|
5
5
|
import { immer } from "zustand/middleware/immer";
|
|
6
|
-
import { Immer, current, enableMapSet, enablePatches, freeze, isDraft } from "immer";
|
|
6
|
+
import { Immer, current, enableMapSet, enablePatches, freeze, isDraft, original } from "immer";
|
|
7
7
|
import { shallow } from "zustand/vanilla/shallow";
|
|
8
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "preact/jsx-runtime";
|
|
9
9
|
import { micromark } from "micromark";
|
|
@@ -10900,6 +10900,13 @@ function registerWidgetDefinitions(nodes, passageName) {
|
|
|
10900
10900
|
*/
|
|
10901
10901
|
var FrozenStateContext = createContext(null);
|
|
10902
10902
|
/**
|
|
10903
|
+
* Tells a timer ({repeat}, {timed}) whether the passage it belongs to has
|
|
10904
|
+
* been left: it must not run a new body then, for that would write into the
|
|
10905
|
+
* destination's state until the outgoing tree unmounts (#410). Null outside
|
|
10906
|
+
* a passage.
|
|
10907
|
+
*/
|
|
10908
|
+
var OutgoingContext = createContext(null);
|
|
10909
|
+
/**
|
|
10903
10910
|
* Story state fields `keys`, re-rendering when any of them changes (as one
|
|
10904
10911
|
* useStoryStore() selector per field would). The result keeps its identity
|
|
10905
10912
|
* while the fields don't change, as the store's snapshot must.
|
|
@@ -14417,7 +14424,12 @@ function getByPath(obj, segments) {
|
|
|
14417
14424
|
for (const seg of segments) {
|
|
14418
14425
|
if (current == null) return void 0;
|
|
14419
14426
|
if (seg in Object.prototype && !hasOwn(Object(current), seg)) return;
|
|
14420
|
-
|
|
14427
|
+
const holder = current;
|
|
14428
|
+
current = holder[seg];
|
|
14429
|
+
if (current === void 0 && isDraft(holder) && (holder instanceof Map || holder instanceof Set)) {
|
|
14430
|
+
const base = original(holder);
|
|
14431
|
+
if (base && hasOwn(base, seg)) current = base[seg];
|
|
14432
|
+
}
|
|
14421
14433
|
}
|
|
14422
14434
|
return current;
|
|
14423
14435
|
}
|
|
@@ -15048,6 +15060,18 @@ function setEntryValue(root, path, value) {
|
|
|
15048
15060
|
return true;
|
|
15049
15061
|
}
|
|
15050
15062
|
/**
|
|
15063
|
+
* The own enumerable fields of a Map, Set, Date or RegExp (an instance of a
|
|
15064
|
+
* registered subclass may hold some besides its built-in content, #412);
|
|
15065
|
+
* none for any other value.
|
|
15066
|
+
*/
|
|
15067
|
+
function builtinFields(value) {
|
|
15068
|
+
return value instanceof Map || value instanceof Set || value instanceof Date || value instanceof RegExp ? extraKeys(value) : [];
|
|
15069
|
+
}
|
|
15070
|
+
/** Whether `x` and `y` are built-ins of one class that may hold fields. */
|
|
15071
|
+
function sameBuiltin(x, y) {
|
|
15072
|
+
return Object.getPrototypeOf(x) === Object.getPrototypeOf(y) && (builtinFields(x).length > 0 || builtinFields(y).length > 0);
|
|
15073
|
+
}
|
|
15074
|
+
/**
|
|
15051
15075
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
15052
15076
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
15053
15077
|
* entered, where first met only (an object held in an array is one object,
|
|
@@ -15071,10 +15095,13 @@ function objectPaths(root, skip = /* @__PURE__ */ new Set(), entries = false) {
|
|
|
15071
15095
|
}
|
|
15072
15096
|
all.set(value, [at]);
|
|
15073
15097
|
if (isMergeable(value) || Array.isArray(value)) walk(value, at);
|
|
15074
|
-
else
|
|
15098
|
+
else {
|
|
15099
|
+
walk(value, at, builtinFields(value));
|
|
15100
|
+
if (entries && entryChildren(value)) collections.push([value, at]);
|
|
15101
|
+
}
|
|
15075
15102
|
}
|
|
15076
|
-
function walk(node, path) {
|
|
15077
|
-
for (const key of
|
|
15103
|
+
function walk(node, path, keys = Object.keys(node)) {
|
|
15104
|
+
for (const key of keys) {
|
|
15078
15105
|
const value = node[key];
|
|
15079
15106
|
if (!isObjectValue(value)) continue;
|
|
15080
15107
|
const at = [...path, key];
|
|
@@ -15122,13 +15149,13 @@ function aliasMoved(x, y, at, earlier, later) {
|
|
|
15122
15149
|
a,
|
|
15123
15150
|
held.get(segment)
|
|
15124
15151
|
]);
|
|
15125
|
-
} else if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true)) {
|
|
15126
|
-
for (const key of Object.keys(y)) if (hasOwn(x, key)) pairs.push([
|
|
15127
|
-
key,
|
|
15128
|
-
x[key],
|
|
15129
|
-
y[key]
|
|
15130
|
-
]);
|
|
15131
15152
|
}
|
|
15153
|
+
const keys = mergesWith(x, y, true) ? Object.keys(y) : sameBuiltin(x, y) ? builtinFields(y) : [];
|
|
15154
|
+
for (const key of keys) if (hasOwn(x, key)) pairs.push([
|
|
15155
|
+
key,
|
|
15156
|
+
x[key],
|
|
15157
|
+
y[key]
|
|
15158
|
+
]);
|
|
15132
15159
|
return pairs.some(([segment, a, b]) => {
|
|
15133
15160
|
if (!isObjectValue(a) || !isObjectValue(b)) return false;
|
|
15134
15161
|
const here = [...at, segment];
|
|
@@ -15149,7 +15176,7 @@ function aliasChanges(before, after) {
|
|
|
15149
15176
|
const later = firstPaths(after, void 0, true);
|
|
15150
15177
|
const changes = [];
|
|
15151
15178
|
(function walk(b, a, path) {
|
|
15152
|
-
for (const key of Object.keys(a)) {
|
|
15179
|
+
for (const key of Array.isArray(a) || isMergeable(a) ? Object.keys(a) : builtinFields(a)) {
|
|
15153
15180
|
if (!hasOwn(b, key)) continue;
|
|
15154
15181
|
const x = b[key];
|
|
15155
15182
|
const y = a[key];
|
|
@@ -15170,7 +15197,7 @@ function aliasChanges(before, after) {
|
|
|
15170
15197
|
value: y,
|
|
15171
15198
|
alias: true
|
|
15172
15199
|
});
|
|
15173
|
-
if ((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
|
|
15200
|
+
if (((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true) || sameBuiltin(x, y)) && pathKey(isAt) === k && pathKey(wasAt) === k) walk(x, y, at);
|
|
15174
15201
|
}
|
|
15175
15202
|
})(before, after, []);
|
|
15176
15203
|
return changes;
|
|
@@ -17923,6 +17950,8 @@ var FOCUSABLE = [
|
|
|
17923
17950
|
"select:not([disabled])",
|
|
17924
17951
|
"textarea:not([disabled])",
|
|
17925
17952
|
"iframe",
|
|
17953
|
+
"audio[controls]",
|
|
17954
|
+
"video[controls]",
|
|
17926
17955
|
"summary",
|
|
17927
17956
|
"[contenteditable]",
|
|
17928
17957
|
"[tabindex]"
|
|
@@ -18822,7 +18851,12 @@ function defineMacro(config, source = "builtin") {
|
|
|
18822
18851
|
}
|
|
18823
18852
|
if (config.storeVar) {
|
|
18824
18853
|
const firstToken = readBoundVariable(props.rawArgs).replace(/["']/g, "");
|
|
18825
|
-
|
|
18854
|
+
const kind = {
|
|
18855
|
+
"%": "transient variables",
|
|
18856
|
+
_: "temporary variables",
|
|
18857
|
+
"@": "locals"
|
|
18858
|
+
}[firstToken.charAt(0)];
|
|
18859
|
+
if (kind) return h("span", { class: "error" }, `{${config.name}}: ${kind} (${firstToken}) cannot be bound to input macros`);
|
|
18826
18860
|
const varExpr = firstToken.replace(/["']/g, "").replace(/^\$/, "");
|
|
18827
18861
|
const segments = varExpr.split(".");
|
|
18828
18862
|
if (segments.includes("__proto__")) return h("span", { class: "error" }, `{${config.name}}: "$${varExpr}" cannot be bound: ${RESERVED_NAME} is reserved`);
|
|
@@ -20406,14 +20440,16 @@ defineMacro({
|
|
|
20406
20440
|
interpolate: true,
|
|
20407
20441
|
parameters: [DELAY_PARAMETER],
|
|
20408
20442
|
render({ children = [] }, ctx) {
|
|
20409
|
-
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
20443
|
+
const { useState, useEffect, useCallback, useContext } = ctx.hooks;
|
|
20410
20444
|
const delay = ctx.args.delay ?? 0;
|
|
20411
20445
|
const [count, setCount] = useState(0);
|
|
20412
20446
|
const [stopped, setStopped] = useState(false);
|
|
20447
|
+
const hasLeft = useContext(OutgoingContext);
|
|
20413
20448
|
const stop = useCallback(() => setStopped(true), []);
|
|
20414
20449
|
useEffect(() => {
|
|
20415
20450
|
if (stopped) return;
|
|
20416
20451
|
const interval = setInterval(() => {
|
|
20452
|
+
if (hasLeft?.()) return;
|
|
20417
20453
|
setCount((c) => c + 1);
|
|
20418
20454
|
}, delay);
|
|
20419
20455
|
return () => clearInterval(interval);
|
|
@@ -20450,7 +20486,8 @@ var frozenAlways = () => true;
|
|
|
20450
20486
|
* the owning macro sees and provides them again: the locals scope (read
|
|
20451
20487
|
* through a live view, so a local assigned earlier in the body is visible),
|
|
20452
20488
|
* nobr/inline rendering, the enclosing block widget's {@children}, the
|
|
20453
|
-
* enclosing {repeat}'s {stop} and the enclosing dialog's close callback
|
|
20489
|
+
* enclosing {repeat}'s {stop} and the enclosing dialog's close callback and whether it runs in a {for} iteration
|
|
20490
|
+
* (#411) and the owning passage's departure (#410).
|
|
20454
20491
|
* The body then behaves as if it rendered in place.
|
|
20455
20492
|
*
|
|
20456
20493
|
* The body's macros read story state from the store. Run from mutation code
|
|
@@ -20465,6 +20502,8 @@ function useDetachedBody() {
|
|
|
20465
20502
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
20466
20503
|
const repeat = useContext(RepeatContext);
|
|
20467
20504
|
const closeDialog = useContext(DialogCloseContext);
|
|
20505
|
+
const inIteration = useContext(ItemEditContext);
|
|
20506
|
+
const hasLeft = useContext(OutgoingContext);
|
|
20468
20507
|
const mounted = useRef([]);
|
|
20469
20508
|
useEffect(() => {
|
|
20470
20509
|
const unsubscribe = subscribeStateReplacement(() => {
|
|
@@ -20497,10 +20536,16 @@ function useDetachedBody() {
|
|
|
20497
20536
|
value: repeat,
|
|
20498
20537
|
children: /* @__PURE__ */ jsx(DialogCloseContext.Provider, {
|
|
20499
20538
|
value: closeDialog,
|
|
20500
|
-
children:
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20539
|
+
children: /* @__PURE__ */ jsx(ItemEditContext.Provider, {
|
|
20540
|
+
value: inIteration,
|
|
20541
|
+
children: /* @__PURE__ */ jsx(OutgoingContext.Provider, {
|
|
20542
|
+
value: hasLeft,
|
|
20543
|
+
children: renderNodes(children, {
|
|
20544
|
+
nobr,
|
|
20545
|
+
inline,
|
|
20546
|
+
locals
|
|
20547
|
+
})
|
|
20548
|
+
})
|
|
20504
20549
|
})
|
|
20505
20550
|
})
|
|
20506
20551
|
})
|
|
@@ -20659,7 +20704,8 @@ defineMacro({
|
|
|
20659
20704
|
interpolate: true,
|
|
20660
20705
|
parameters: TIMED_PARAMETERS,
|
|
20661
20706
|
render({ branches = [] }, ctx) {
|
|
20662
|
-
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
20707
|
+
const { useState, useEffect, useMemo, useContext } = ctx.hooks;
|
|
20708
|
+
const hasLeft = useContext(OutgoingContext);
|
|
20663
20709
|
const sections = useMemo(() => {
|
|
20664
20710
|
return branches.map((branch) => ({
|
|
20665
20711
|
delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
|
|
@@ -20674,6 +20720,7 @@ defineMacro({
|
|
|
20674
20720
|
const nextIndex = visibleIndex + 1;
|
|
20675
20721
|
const delay = sections[nextIndex].delay;
|
|
20676
20722
|
const timer = setTimeout(() => {
|
|
20723
|
+
if (hasLeft?.()) return;
|
|
20677
20724
|
setVisibleIndex(nextIndex);
|
|
20678
20725
|
}, delay);
|
|
20679
20726
|
return () => clearTimeout(timer);
|
|
@@ -21981,6 +22028,7 @@ defineMacro({
|
|
|
21981
22028
|
const prevHistoryLenRef = useRef(0);
|
|
21982
22029
|
const displayedId = displayed.id;
|
|
21983
22030
|
const isOutgoing = useCallback((state) => state.navigationId !== displayedId, [displayedId]);
|
|
22031
|
+
const hasLeft = useCallback(() => isOutgoing(useStoryStore.getState()), [isOutgoing]);
|
|
21984
22032
|
const lastElement = useRef(null);
|
|
21985
22033
|
const containerRef = useRef(null);
|
|
21986
22034
|
const timeoutsRef = useRef([]);
|
|
@@ -22087,7 +22135,10 @@ defineMacro({
|
|
|
22087
22135
|
ref: containerRef,
|
|
22088
22136
|
children: passageElement && /* @__PURE__ */ jsx(FrozenStateContext.Provider, {
|
|
22089
22137
|
value: isOutgoing,
|
|
22090
|
-
children:
|
|
22138
|
+
children: /* @__PURE__ */ jsx(OutgoingContext.Provider, {
|
|
22139
|
+
value: hasLeft,
|
|
22140
|
+
children: passageElement
|
|
22141
|
+
})
|
|
22091
22142
|
})
|
|
22092
22143
|
})]
|
|
22093
22144
|
});
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import { useStoryStore, type StoryState } from '../../store';
|
|
|
9
9
|
import {
|
|
10
10
|
useStoryFields,
|
|
11
11
|
FrozenStateContext,
|
|
12
|
+
OutgoingContext,
|
|
12
13
|
} from '../../hooks/use-story-fields';
|
|
13
14
|
import { Passage, renderPassageContent } from '../Passage';
|
|
14
15
|
import { defineMacro } from '../../define-macro';
|
|
@@ -140,6 +141,11 @@ defineMacro({
|
|
|
140
141
|
(state: StoryState) => state.navigationId !== displayedId,
|
|
141
142
|
[displayedId],
|
|
142
143
|
);
|
|
144
|
+
// Read live: a timer asks it before the outgoing tree has unmounted
|
|
145
|
+
const hasLeft = useCallback(
|
|
146
|
+
() => isOutgoing(useStoryStore.getState()),
|
|
147
|
+
[isOutgoing],
|
|
148
|
+
);
|
|
143
149
|
const lastElement = useRef<preact.VNode | null>(null);
|
|
144
150
|
|
|
145
151
|
// Container ref for snapshot insertion
|
|
@@ -350,7 +356,9 @@ defineMacro({
|
|
|
350
356
|
>
|
|
351
357
|
{passageElement && (
|
|
352
358
|
<FrozenStateContext.Provider value={isOutgoing}>
|
|
353
|
-
{
|
|
359
|
+
<OutgoingContext.Provider value={hasLeft}>
|
|
360
|
+
{passageElement}
|
|
361
|
+
</OutgoingContext.Provider>
|
|
354
362
|
</FrozenStateContext.Provider>
|
|
355
363
|
)}
|
|
356
364
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createContext } from 'preact';
|
|
2
2
|
import { defineMacro } from '../../define-macro';
|
|
3
3
|
import { DELAY_PARAMETER } from './macro-args';
|
|
4
|
+
import { OutgoingContext } from '../../hooks/use-story-fields';
|
|
4
5
|
|
|
5
6
|
export const RepeatContext = createContext<{ stop: () => void }>({
|
|
6
7
|
stop: () => {},
|
|
@@ -12,17 +13,21 @@ defineMacro({
|
|
|
12
13
|
interpolate: true,
|
|
13
14
|
parameters: [DELAY_PARAMETER],
|
|
14
15
|
render({ children = [] }, ctx) {
|
|
15
|
-
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
16
|
+
const { useState, useEffect, useCallback, useContext } = ctx.hooks;
|
|
16
17
|
|
|
17
18
|
const delay = ctx.args.delay ?? 0;
|
|
18
19
|
const [count, setCount] = useState(0);
|
|
19
20
|
const [stopped, setStopped] = useState(false);
|
|
20
21
|
|
|
22
|
+
const hasLeft = useContext(OutgoingContext);
|
|
23
|
+
|
|
21
24
|
const stop = useCallback(() => setStopped(true), []);
|
|
22
25
|
|
|
23
26
|
useEffect(() => {
|
|
24
27
|
if (stopped) return;
|
|
25
28
|
const interval = setInterval(() => {
|
|
29
|
+
// The passage was left: its body must not run again (#410)
|
|
30
|
+
if (hasLeft?.()) return;
|
|
26
31
|
setCount((c) => c + 1);
|
|
27
32
|
}, delay);
|
|
28
33
|
return () => clearInterval(interval);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { DELAY_PARAMETER, parseMacroArgs } from './macro-args';
|
|
3
3
|
import { wrapContent } from './display';
|
|
4
|
+
import { OutgoingContext } from '../../hooks/use-story-fields';
|
|
4
5
|
|
|
5
6
|
/** The parameters of {timed} and of each {next}: the delay before it shows. */
|
|
6
7
|
const TIMED_PARAMETERS = [DELAY_PARAMETER] as const;
|
|
@@ -11,7 +12,8 @@ defineMacro({
|
|
|
11
12
|
interpolate: true,
|
|
12
13
|
parameters: TIMED_PARAMETERS,
|
|
13
14
|
render({ branches = [] }, ctx) {
|
|
14
|
-
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
15
|
+
const { useState, useEffect, useMemo, useContext } = ctx.hooks;
|
|
16
|
+
const hasLeft = useContext(OutgoingContext);
|
|
15
17
|
|
|
16
18
|
const sections = useMemo(() => {
|
|
17
19
|
return branches.map((branch) => ({
|
|
@@ -31,6 +33,8 @@ defineMacro({
|
|
|
31
33
|
const delay = sections[nextIndex]!.delay;
|
|
32
34
|
|
|
33
35
|
const timer = setTimeout(() => {
|
|
36
|
+
// The passage was left: its body must not run (#410)
|
|
37
|
+
if (hasLeft?.()) return;
|
|
34
38
|
setVisibleIndex(nextIndex);
|
|
35
39
|
}, delay);
|
|
36
40
|
|
|
@@ -10,12 +10,16 @@ import {
|
|
|
10
10
|
} from '../../markup/render';
|
|
11
11
|
import type { ASTNode } from '../../markup/ast';
|
|
12
12
|
import { useRenderOptions } from '../../hooks/use-render-options';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
FrozenStateContext,
|
|
15
|
+
OutgoingContext,
|
|
16
|
+
} from '../../hooks/use-story-fields';
|
|
14
17
|
import { liveLocalsView } from '../../utils/live-locals';
|
|
15
18
|
import { runWithCommittedMutations } from '../../execute-mutation';
|
|
16
19
|
import { subscribeStateReplacement } from '../../store';
|
|
17
20
|
import { RepeatContext } from './Repeat';
|
|
18
21
|
import { DialogCloseContext } from '../PassageDialog';
|
|
22
|
+
import { ItemEditContext } from '../../utils/control-edits';
|
|
19
23
|
|
|
20
24
|
const frozenAlways = () => true;
|
|
21
25
|
|
|
@@ -35,7 +39,8 @@ const frozenAlways = () => true;
|
|
|
35
39
|
* the owning macro sees and provides them again: the locals scope (read
|
|
36
40
|
* through a live view, so a local assigned earlier in the body is visible),
|
|
37
41
|
* nobr/inline rendering, the enclosing block widget's {@children}, the
|
|
38
|
-
* enclosing {repeat}'s {stop} and the enclosing dialog's close callback
|
|
42
|
+
* enclosing {repeat}'s {stop} and the enclosing dialog's close callback and whether it runs in a {for} iteration
|
|
43
|
+
* (#411) and the owning passage's departure (#410).
|
|
39
44
|
* The body then behaves as if it rendered in place.
|
|
40
45
|
*
|
|
41
46
|
* The body's macros read story state from the store. Run from mutation code
|
|
@@ -50,6 +55,8 @@ export function useDetachedBody(): (children: ASTNode[]) => void {
|
|
|
50
55
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
51
56
|
const repeat = useContext(RepeatContext);
|
|
52
57
|
const closeDialog = useContext(DialogCloseContext);
|
|
58
|
+
const inIteration = useContext(ItemEditContext);
|
|
59
|
+
const hasLeft = useContext(OutgoingContext);
|
|
53
60
|
|
|
54
61
|
const mounted = useRef<HTMLElement[]>([]);
|
|
55
62
|
useEffect(() => {
|
|
@@ -81,7 +88,11 @@ export function useDetachedBody(): (children: ASTNode[]) => void {
|
|
|
81
88
|
<WidgetChildrenContext.Provider value={widgetChildren}>
|
|
82
89
|
<RepeatContext.Provider value={repeat}>
|
|
83
90
|
<DialogCloseContext.Provider value={closeDialog}>
|
|
84
|
-
|
|
91
|
+
<ItemEditContext.Provider value={inIteration}>
|
|
92
|
+
<OutgoingContext.Provider value={hasLeft}>
|
|
93
|
+
{renderNodes(children, { nobr, inline, locals })}
|
|
94
|
+
</OutgoingContext.Provider>
|
|
95
|
+
</ItemEditContext.Provider>
|
|
85
96
|
</DialogCloseContext.Provider>
|
|
86
97
|
</RepeatContext.Provider>
|
|
87
98
|
</WidgetChildrenContext.Provider>
|
package/src/define-macro.ts
CHANGED
|
@@ -216,11 +216,19 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
|
|
|
216
216
|
if (config.storeVar) {
|
|
217
217
|
const firstToken = readBoundVariable(props.rawArgs).replace(/["']/g, '');
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
// Only story variables ($name) can be bound: another sigil would be
|
|
220
|
+
// read as a story key spelled with it (#414)
|
|
221
|
+
const unbindable: Record<string, string> = {
|
|
222
|
+
'%': 'transient variables',
|
|
223
|
+
_: 'temporary variables',
|
|
224
|
+
'@': 'locals',
|
|
225
|
+
};
|
|
226
|
+
const kind = unbindable[firstToken.charAt(0)];
|
|
227
|
+
if (kind) {
|
|
220
228
|
return h(
|
|
221
229
|
'span',
|
|
222
230
|
{ class: 'error' },
|
|
223
|
-
`{${config.name}}:
|
|
231
|
+
`{${config.name}}: ${kind} (${firstToken}) cannot be bound to input macros`,
|
|
224
232
|
);
|
|
225
233
|
}
|
|
226
234
|
|
|
@@ -15,6 +15,14 @@ export const FrozenStateContext = createContext<
|
|
|
15
15
|
((state: StoryState) => boolean) | null
|
|
16
16
|
>(null);
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Tells a timer ({repeat}, {timed}) whether the passage it belongs to has
|
|
20
|
+
* been left: it must not run a new body then, for that would write into the
|
|
21
|
+
* destination's state until the outgoing tree unmounts (#410). Null outside
|
|
22
|
+
* a passage.
|
|
23
|
+
*/
|
|
24
|
+
export const OutgoingContext = createContext<(() => boolean) | null>(null);
|
|
25
|
+
|
|
18
26
|
/**
|
|
19
27
|
* Story state fields `keys`, re-rendering when any of them changes (as one
|
|
20
28
|
* useStoryStore() selector per field would). The result keeps its identity
|
package/src/structural.ts
CHANGED
|
@@ -859,6 +859,28 @@ function setEntryValue(
|
|
|
859
859
|
return true;
|
|
860
860
|
}
|
|
861
861
|
|
|
862
|
+
/**
|
|
863
|
+
* The own enumerable fields of a Map, Set, Date or RegExp (an instance of a
|
|
864
|
+
* registered subclass may hold some besides its built-in content, #412);
|
|
865
|
+
* none for any other value.
|
|
866
|
+
*/
|
|
867
|
+
function builtinFields(value: object): string[] {
|
|
868
|
+
return value instanceof Map ||
|
|
869
|
+
value instanceof Set ||
|
|
870
|
+
value instanceof Date ||
|
|
871
|
+
value instanceof RegExp
|
|
872
|
+
? extraKeys(value)
|
|
873
|
+
: [];
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Whether `x` and `y` are built-ins of one class that may hold fields. */
|
|
877
|
+
function sameBuiltin(x: object, y: object): boolean {
|
|
878
|
+
return (
|
|
879
|
+
Object.getPrototypeOf(x) === Object.getPrototypeOf(y) &&
|
|
880
|
+
(builtinFields(x).length > 0 || builtinFields(y).length > 0)
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
862
884
|
/**
|
|
863
885
|
* Every path each object (or array, or other value) of `root` is at, the
|
|
864
886
|
* first appearance first, in depth-first order. Plain objects and arrays are
|
|
@@ -888,12 +910,17 @@ function objectPaths(
|
|
|
888
910
|
all.set(value, [at]);
|
|
889
911
|
if (isMergeable(value) || Array.isArray(value)) {
|
|
890
912
|
walk(value as Record<string, unknown>, at);
|
|
891
|
-
} else
|
|
892
|
-
|
|
913
|
+
} else {
|
|
914
|
+
walk(value as Record<string, unknown>, at, builtinFields(value));
|
|
915
|
+
if (entries && entryChildren(value)) collections.push([value, at]);
|
|
893
916
|
}
|
|
894
917
|
}
|
|
895
|
-
function walk(
|
|
896
|
-
|
|
918
|
+
function walk(
|
|
919
|
+
node: Record<string, unknown>,
|
|
920
|
+
path: string[],
|
|
921
|
+
keys: string[] = Object.keys(node),
|
|
922
|
+
): void {
|
|
923
|
+
for (const key of keys) {
|
|
897
924
|
const value = node[key];
|
|
898
925
|
if (!isObjectValue(value)) continue;
|
|
899
926
|
const at = [...path, key];
|
|
@@ -963,12 +990,16 @@ function aliasMoved(
|
|
|
963
990
|
for (const [segment, a] of xs) {
|
|
964
991
|
if (held.has(segment)) pairs.push([segment, a, held.get(segment)]);
|
|
965
992
|
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
993
|
+
}
|
|
994
|
+
// The properties both hold: of objects that merge, and the fields of a
|
|
995
|
+
// built-in subclass instance besides its entries (#412)
|
|
996
|
+
const keys = mergesWith(x, y, true)
|
|
997
|
+
? Object.keys(y)
|
|
998
|
+
: sameBuiltin(x, y)
|
|
999
|
+
? builtinFields(y)
|
|
1000
|
+
: [];
|
|
1001
|
+
for (const key of keys) {
|
|
1002
|
+
if (hasOwn(x, key)) pairs.push([key, (x as any)[key], (y as any)[key]]);
|
|
972
1003
|
}
|
|
973
1004
|
return pairs.some(([segment, a, b]) => {
|
|
974
1005
|
if (!isObjectValue(a) || !isObjectValue(b)) return false;
|
|
@@ -1002,7 +1033,9 @@ export function aliasChanges(
|
|
|
1002
1033
|
a: Record<string, unknown>,
|
|
1003
1034
|
path: string[],
|
|
1004
1035
|
): void {
|
|
1005
|
-
for (const key of
|
|
1036
|
+
for (const key of Array.isArray(a) || isMergeable(a)
|
|
1037
|
+
? Object.keys(a)
|
|
1038
|
+
: builtinFields(a)) {
|
|
1006
1039
|
if (!hasOwn(b, key)) continue;
|
|
1007
1040
|
const x = b[key];
|
|
1008
1041
|
const y = a[key];
|
|
@@ -1023,8 +1056,8 @@ export function aliasChanges(
|
|
|
1023
1056
|
// Only paths that are first appearances are entered: elsewhere the
|
|
1024
1057
|
// contents are those of the object met first.
|
|
1025
1058
|
if (
|
|
1026
|
-
(isMergeable(x) || Array.isArray(x)) &&
|
|
1027
|
-
|
|
1059
|
+
(((isMergeable(x) || Array.isArray(x)) && mergesWith(x, y, true)) ||
|
|
1060
|
+
sameBuiltin(x, y)) &&
|
|
1028
1061
|
pathKey(isAt) === k &&
|
|
1029
1062
|
pathKey(wasAt) === k
|
|
1030
1063
|
) {
|
package/src/utils/object-path.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { current as draftState, isDraft } from 'immer';
|
|
1
|
+
import { current as draftState, isDraft, original } from 'immer';
|
|
2
2
|
import { registeredClassName } from '../class-registry';
|
|
3
3
|
import { hasOwn } from './namespace';
|
|
4
4
|
import { atomicName } from './value-kinds';
|
|
@@ -18,7 +18,18 @@ export function getByPath(obj: object, segments: readonly string[]): unknown {
|
|
|
18
18
|
if (seg in Object.prototype && !hasOwn(Object(current), seg)) {
|
|
19
19
|
return undefined;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
const holder = current as Record<string, unknown>;
|
|
22
|
+
current = holder[seg];
|
|
23
|
+
// An Immer draft of a Map or Set does not carry the own properties of a
|
|
24
|
+
// registered subclass instance: they are those of its base (#412)
|
|
25
|
+
if (
|
|
26
|
+
current === undefined &&
|
|
27
|
+
isDraft(holder) &&
|
|
28
|
+
(holder instanceof Map || holder instanceof Set)
|
|
29
|
+
) {
|
|
30
|
+
const base = original(holder) as Record<string, unknown> | undefined;
|
|
31
|
+
if (base && hasOwn(base, seg)) current = base[seg];
|
|
32
|
+
}
|
|
22
33
|
}
|
|
23
34
|
return current;
|
|
24
35
|
}
|