@gjermundgaraba-designs/pixel-paper 0.22.2 → 0.24.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/DESIGN.md +18 -17
- package/dist/src/adapters/mermaid.d.ts.map +1 -1
- package/dist/src/adapters/mermaid.js +0 -24
- package/dist/src/announcer/announcer.d.ts.map +1 -1
- package/dist/src/announcer/announcer.js +12 -12
- package/dist/src/board/board.d.ts +0 -10
- package/dist/src/board/board.d.ts.map +1 -1
- package/dist/src/board/board.js +36 -64
- package/dist/src/board/index.d.ts +2 -1
- package/dist/src/board/index.d.ts.map +1 -1
- package/dist/src/board/index.js +2 -1
- package/dist/src/button/button.d.ts +1 -1
- package/dist/src/button/button.d.ts.map +1 -1
- package/dist/src/commandPalette/commandPalette.d.ts.map +1 -1
- package/dist/src/commandPalette/commandPalette.js +78 -83
- package/dist/src/commandPalette/keys.d.ts +2 -2
- package/dist/src/commandPalette/keys.d.ts.map +1 -1
- package/dist/src/commandPalette/keys.js +2 -4
- package/dist/src/commandPalette/match.d.ts.map +1 -1
- package/dist/src/commandPalette/match.js +2 -2
- package/dist/src/confirmDialog/confirmDialog.d.ts.map +1 -1
- package/dist/src/confirmDialog/confirmDialog.js +1 -2
- package/dist/src/copyButton/copyButton.d.ts +0 -5
- package/dist/src/copyButton/copyButton.d.ts.map +1 -1
- package/dist/src/copyButton/copyButton.js +2 -2
- package/dist/src/crumbs/crumbs.d.ts +0 -2
- package/dist/src/crumbs/crumbs.d.ts.map +1 -1
- package/dist/src/crumbs/crumbs.js +2 -2
- package/dist/src/dialog/dialog.d.ts +2 -0
- package/dist/src/dialog/dialog.d.ts.map +1 -1
- package/dist/src/dialog/dialog.js +3 -2
- package/dist/src/drag/drag.d.ts +28 -0
- package/dist/src/drag/drag.d.ts.map +1 -1
- package/dist/src/drag/drag.js +39 -17
- package/dist/src/editableTitle/editableTitle.d.ts +4 -4
- package/dist/src/field/field.d.ts +7 -1
- package/dist/src/field/field.d.ts.map +1 -1
- package/dist/src/field/field.js +14 -6
- package/dist/src/fieldset/fieldset.d.ts.map +1 -1
- package/dist/src/fieldset/fieldset.js +2 -4
- package/dist/src/fileTree/fileTree.d.ts.map +1 -1
- package/dist/src/fileTree/fileTree.js +16 -16
- package/dist/src/iconButton/iconButton.d.ts +6 -1
- package/dist/src/iconButton/iconButton.d.ts.map +1 -1
- package/dist/src/iconButton/iconButton.js +7 -2
- package/dist/src/inlineInput/inlineInput.d.ts +4 -0
- package/dist/src/inlineInput/inlineInput.d.ts.map +1 -1
- package/dist/src/inlineInput/inlineInput.js +9 -6
- package/dist/src/keyHelp/index.d.ts +1 -1
- package/dist/src/keyHelp/index.d.ts.map +1 -1
- package/dist/src/keyHelp/index.js +1 -1
- package/dist/src/keyHelp/keyHelp.d.ts +1 -3
- package/dist/src/keyHelp/keyHelp.d.ts.map +1 -1
- package/dist/src/keyHelp/keyHelp.js +3 -6
- package/dist/src/keyHint/keyLabel.d.ts +2 -0
- package/dist/src/keyHint/keyLabel.d.ts.map +1 -1
- package/dist/src/keyHint/keyLabel.js +3 -2
- package/dist/src/lineDiff/diff.d.ts +6 -3
- package/dist/src/lineDiff/diff.d.ts.map +1 -1
- package/dist/src/lineDiff/diff.js +40 -28
- package/dist/src/lineDiff/patch.d.ts.map +1 -1
- package/dist/src/lineDiff/patch.js +18 -13
- package/dist/src/linkState/linkState.d.ts.map +1 -1
- package/dist/src/linkState/linkState.js +1 -7
- package/dist/src/menu/slip.js +2 -2
- package/dist/src/meter/meter.d.ts.map +1 -1
- package/dist/src/meter/meter.js +22 -17
- package/dist/src/notice/notice.d.ts +2 -0
- package/dist/src/notice/notice.d.ts.map +1 -1
- package/dist/src/notice/notice.js +2 -1
- package/dist/src/panel/panel.d.ts +3 -0
- package/dist/src/panel/panel.d.ts.map +1 -1
- package/dist/src/panel/panel.js +2 -1
- package/dist/src/passwordField/passwordField.d.ts.map +1 -1
- package/dist/src/passwordField/passwordField.js +3 -7
- package/dist/src/rail/index.d.ts +1 -1
- package/dist/src/rail/index.d.ts.map +1 -1
- package/dist/src/rail/index.js +1 -1
- package/dist/src/rail/rail.d.ts +95 -20
- package/dist/src/rail/rail.d.ts.map +1 -1
- package/dist/src/rail/rail.js +103 -46
- package/dist/src/reel/layout.d.ts +9 -5
- package/dist/src/reel/layout.d.ts.map +1 -1
- package/dist/src/reel/layout.js +11 -5
- package/dist/src/reel/reel.d.ts.map +1 -1
- package/dist/src/reel/reel.js +122 -68
- package/dist/src/scheme/scheme.d.ts +1 -1
- package/dist/src/scheme/scheme.d.ts.map +1 -1
- package/dist/src/segmented/segmented.d.ts +0 -3
- package/dist/src/segmented/segmented.d.ts.map +1 -1
- package/dist/src/segmented/segmented.js +1 -6
- package/dist/src/selectField/selectField.d.ts.map +1 -1
- package/dist/src/selectField/selectField.js +3 -10
- package/dist/src/sortable/index.d.ts +1 -1
- package/dist/src/sortable/index.d.ts.map +1 -1
- package/dist/src/sortable/index.js +1 -1
- package/dist/src/sortable/sortable.d.ts +39 -40
- package/dist/src/sortable/sortable.d.ts.map +1 -1
- package/dist/src/sortable/sortable.js +14 -31
- package/dist/src/stack/stack.d.ts +1 -1
- package/dist/src/stack/stack.d.ts.map +1 -1
- package/dist/src/stack/stack.js +1 -1
- package/dist/src/tabs/tabs.d.ts +1 -1
- package/dist/src/tabs/tabs.d.ts.map +1 -1
- package/dist/src/tabs/tabs.js +1 -1
- package/dist/src/textareaField/textareaField.d.ts.map +1 -1
- package/dist/src/textareaField/textareaField.js +3 -7
- package/dist/src/time/time.d.ts.map +1 -1
- package/dist/src/time/time.js +4 -2
- package/dist/src/tips/index.d.ts +1 -1
- package/dist/src/tips/index.d.ts.map +1 -1
- package/dist/src/tips/tips.d.ts +30 -1
- package/dist/src/tips/tips.d.ts.map +1 -1
- package/dist/src/tips/tips.js +53 -31
- package/dist/src/toast/index.d.ts +1 -1
- package/dist/src/toast/index.d.ts.map +1 -1
- package/dist/src/toast/toast.d.ts +5 -5
- package/dist/src/toast/toast.d.ts.map +1 -1
- package/dist/src/toast/toast.js +1 -6
- package/dist/src/toolbar/toolbar.d.ts +0 -3
- package/dist/src/toolbar/toolbar.d.ts.map +1 -1
- package/dist/src/toolbar/toolbar.js +1 -2
- package/package.json +1 -1
- package/src/adapters/mermaid.ts +0 -24
- package/src/announcer/announcer.ts +13 -17
- package/src/board/board.ts +32 -80
- package/src/board/index.ts +1 -1
- package/src/button/button.ts +1 -1
- package/src/commandPalette/commandPalette.ts +21 -38
- package/src/commandPalette/keys.ts +2 -4
- package/src/commandPalette/match.ts +3 -3
- package/src/confirmDialog/confirmDialog.ts +5 -3
- package/src/copyButton/copyButton.ts +2 -5
- package/src/crumbs/crumbs.ts +2 -4
- package/src/dialog/dialog.ts +3 -2
- package/src/drag/drag.ts +54 -5
- package/src/field/field.ts +29 -7
- package/src/fieldset/fieldset.ts +2 -4
- package/src/fileTree/fileTree.ts +10 -12
- package/src/iconButton/iconButton.ts +19 -2
- package/src/inlineInput/inlineInput.ts +11 -2
- package/src/keyHelp/index.ts +0 -1
- package/src/keyHelp/keyHelp.ts +3 -9
- package/src/keyHint/keyLabel.ts +5 -2
- package/src/lineDiff/diff.ts +70 -54
- package/src/lineDiff/patch.ts +27 -14
- package/src/linkState/linkState.ts +1 -8
- package/src/menu/slip.ts +2 -2
- package/src/meter/meter.ts +25 -15
- package/src/notice/notice.ts +2 -1
- package/src/panel/panel.ts +5 -0
- package/src/passwordField/passwordField.ts +3 -7
- package/src/pixel-paper.css +200 -134
- package/src/rail/index.ts +2 -0
- package/src/rail/rail.ts +124 -44
- package/src/reel/layout.ts +14 -7
- package/src/reel/reel.ts +132 -63
- package/src/scheme/scheme.ts +1 -1
- package/src/segmented/segmented.ts +1 -9
- package/src/selectField/selectField.ts +3 -10
- package/src/sortable/index.ts +0 -1
- package/src/sortable/sortable.ts +35 -54
- package/src/stack/stack.ts +1 -1
- package/src/tabs/tabs.ts +1 -1
- package/src/textareaField/textareaField.ts +3 -7
- package/src/time/time.ts +6 -7
- package/src/tips/index.ts +1 -1
- package/src/tips/tips.ts +71 -35
- package/src/toast/index.ts +1 -1
- package/src/toast/toast.ts +2 -8
- package/src/tokens.css +7 -0
- package/src/toolbar/toolbar.ts +0 -5
- package/dist/src/inlineInput/inline.d.ts +0 -6
- package/dist/src/inlineInput/inline.d.ts.map +0 -1
- package/dist/src/inlineInput/inline.js +0 -7
- package/src/inlineInput/inline.ts +0 -13
|
@@ -35,7 +35,7 @@ export const view = <Message>(h: HtmlBuilder<Message>): Html =>
|
|
|
35
35
|
h.Class("sr-only"),
|
|
36
36
|
h.Role("status"),
|
|
37
37
|
h.AriaLive("polite"),
|
|
38
|
-
h.
|
|
38
|
+
h.Attribute(ANNOUNCER_ATTRIBUTE, ""),
|
|
39
39
|
]);
|
|
40
40
|
|
|
41
41
|
// ANNOUNCE
|
|
@@ -97,28 +97,21 @@ export const announce = (text: string): Effect.Effect<void> =>
|
|
|
97
97
|
// FLASH
|
|
98
98
|
|
|
99
99
|
const FLASH_ID = "pixel-paper-flash";
|
|
100
|
-
const DEFAULT_BEAT = Duration.millis(700);
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
/** The beat in milliseconds, from the stylesheet's `--motion-flash`: none on a page that has
|
|
102
|
+
* not loaded the stylesheet, which has nothing to flash with. */
|
|
103
|
+
const beatOf = (element: Element): Option.Option<number> => {
|
|
104
|
+
const time = getComputedStyle(element).getPropertyValue("--motion-flash").trim();
|
|
105
|
+
return pipe(
|
|
104
106
|
Option.liftPredicate(Number.parseFloat(time), Number.isFinite),
|
|
105
107
|
Option.map((value) => (time.endsWith("ms") ? value : value * 1000)),
|
|
106
108
|
);
|
|
107
|
-
|
|
108
|
-
const beatOf = (element: Element): number =>
|
|
109
|
-
Option.getOrElse(
|
|
110
|
-
toMillis(getComputedStyle(element).getPropertyValue("--motion-flash").trim()),
|
|
111
|
-
() => Duration.toMillis(DEFAULT_BEAT),
|
|
112
|
-
);
|
|
109
|
+
};
|
|
113
110
|
|
|
114
111
|
const prefersReducedMotion = (): boolean =>
|
|
115
112
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
116
113
|
|
|
117
|
-
const runFlash = (element: Element): void => {
|
|
118
|
-
if (!("animate" in element)) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const beat = beatOf(element);
|
|
114
|
+
const runFlash = (element: Element, beat: number): void => {
|
|
122
115
|
Array.forEach(
|
|
123
116
|
Array.filter(element.getAnimations(), (animation) => animation.id === FLASH_ID),
|
|
124
117
|
(animation) => animation.cancel(),
|
|
@@ -141,8 +134,11 @@ const runFlash = (element: Element): void => {
|
|
|
141
134
|
export const flash = (selector: string): Effect.Effect<void> =>
|
|
142
135
|
Effect.sync(() => {
|
|
143
136
|
const maybeElement = Option.fromNullishOr(document.querySelector(selector));
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
const maybeFlash = Option.flatMap(maybeElement, (element) =>
|
|
138
|
+
Option.map(beatOf(element), (beat) => ({ element, beat })),
|
|
139
|
+
);
|
|
140
|
+
if (Option.isSome(maybeFlash)) {
|
|
141
|
+
runFlash(maybeFlash.value.element, maybeFlash.value.beat);
|
|
146
142
|
}
|
|
147
143
|
});
|
|
148
144
|
|
package/src/board/board.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button as UiButton, DragAndDrop as UiDragAndDrop } from "@foldkit/ui";
|
|
2
|
-
import { Array, Effect, Equal, Match, Option, Schema,
|
|
2
|
+
import { Array, Effect, Equal, Match, Option, Schema, pipe } from "effect";
|
|
3
3
|
import * as Command from "foldkit/command";
|
|
4
4
|
import {
|
|
5
5
|
type Attribute,
|
|
@@ -26,13 +26,15 @@ import { prefixKeys } from "../subscription/prefixKeys.ts";
|
|
|
26
26
|
import {
|
|
27
27
|
LEFT_BUTTON,
|
|
28
28
|
activatedKeyboardDrag,
|
|
29
|
+
canLift,
|
|
29
30
|
cancelledDrag,
|
|
30
|
-
|
|
31
|
+
escapeSubscriptions,
|
|
31
32
|
innerControls,
|
|
32
33
|
isCarrying,
|
|
33
34
|
measureLift,
|
|
34
35
|
phaseOf,
|
|
35
36
|
pressedDraggable,
|
|
37
|
+
settle as settleDrag,
|
|
36
38
|
} from "../drag/drag.ts";
|
|
37
39
|
|
|
38
40
|
// MODEL
|
|
@@ -50,19 +52,20 @@ export const Column = Schema.Struct({
|
|
|
50
52
|
});
|
|
51
53
|
export type Column = typeof Column.Type;
|
|
52
54
|
|
|
55
|
+
/** How the keyboard carries what it holds: said on the grip and once it is picked up. */
|
|
56
|
+
const KEYBOARD_MOVES = "Arrow keys move it to another column, space drops it, escape cancels.";
|
|
57
|
+
|
|
53
58
|
/** The words a board says. `{card}` stands for the card's label and `{column}` for a
|
|
54
59
|
* column's title. */
|
|
55
60
|
const COPY = {
|
|
56
61
|
/** The grip's name. */
|
|
57
62
|
gripLabel: "Move {card}",
|
|
58
63
|
/** What the grip describes: how to move a card from the keyboard. */
|
|
59
|
-
gripHelp:
|
|
60
|
-
"Press space to pick the card up. Arrow keys move it to another column, space drops it, escape cancels.",
|
|
64
|
+
gripHelp: `Press space to pick the card up. ${KEYBOARD_MOVES}`,
|
|
61
65
|
/** A pointer has lifted the card. */
|
|
62
66
|
pickedUp: "Picked up {card}.",
|
|
63
67
|
/** The keyboard has lifted the card: say how to go on. */
|
|
64
|
-
pickedUpByKeyboard:
|
|
65
|
-
"Picked up {card}. Arrow keys move it to another column, space drops it, escape cancels.",
|
|
68
|
+
pickedUpByKeyboard: `Picked up {card}. ${KEYBOARD_MOVES}`,
|
|
66
69
|
/** The held card is over another column. */
|
|
67
70
|
over: "{card} is over {column}.",
|
|
68
71
|
/** The card was dropped in a column. */
|
|
@@ -177,12 +180,6 @@ export const id = (model: Model): string => model.dragAndDrop.id;
|
|
|
177
180
|
/** Whether a card is held by a pointer or the keyboard. */
|
|
178
181
|
export const isDragging = (model: Model): boolean => UiDragAndDrop.isDragging(model.dragAndDrop);
|
|
179
182
|
|
|
180
|
-
/** Whether a card is pressed or held. While it is, the arrow keys, Space and Escape belong to
|
|
181
|
-
* the board: pause the page's own bindings for them. The board takes Escape before anything
|
|
182
|
-
* else sees it. */
|
|
183
|
-
export const isHolding = (model: Model): boolean =>
|
|
184
|
-
Option.isSome(UiDragAndDrop.maybeDraggedItemId(model.dragAndDrop));
|
|
185
|
-
|
|
186
183
|
// DOM HOOKS
|
|
187
184
|
|
|
188
185
|
const hookId = (boardId: string, localId: string): string => `${boardId}/${localId}`;
|
|
@@ -234,9 +231,6 @@ const overColumn = (model: Model): Option.Option<Column> =>
|
|
|
234
231
|
),
|
|
235
232
|
);
|
|
236
233
|
|
|
237
|
-
const announceMove = (maybeText: Option.Option<string>): ReadonlyArray<Command.Command<Message>> =>
|
|
238
|
-
Array.fromOption(Option.map(maybeText, (text) => AnnounceMove({ text })));
|
|
239
|
-
|
|
240
234
|
const placementOf = (
|
|
241
235
|
model: Model,
|
|
242
236
|
held: Held,
|
|
@@ -263,10 +257,7 @@ const placementOf = (
|
|
|
263
257
|
fromIndex: from.index,
|
|
264
258
|
toColumnId: to.id,
|
|
265
259
|
toIndex,
|
|
266
|
-
maybeAfterCardId: Option.map(
|
|
267
|
-
toIndex > 0 ? Array.get(others, toIndex - 1) : Option.none(),
|
|
268
|
-
(card) => card.id,
|
|
269
|
-
),
|
|
260
|
+
maybeAfterCardId: Option.map(Array.get(others, toIndex - 1), (card) => card.id),
|
|
270
261
|
maybeBeforeCardId: Option.map(Array.get(others, toIndex), (card) => card.id),
|
|
271
262
|
});
|
|
272
263
|
});
|
|
@@ -349,35 +340,26 @@ const moveText = (
|
|
|
349
340
|
* A card set down without a move says `putBack`. */
|
|
350
341
|
const settle =
|
|
351
342
|
(previous: Model, putBack = COPY.putBack) =>
|
|
352
|
-
(dragAndDropUpdate: UpdateReturn): UpdateReturn =>
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
343
|
+
(dragAndDropUpdate: UpdateReturn): UpdateReturn =>
|
|
344
|
+
settleDrag(
|
|
345
|
+
dragAndDropUpdate,
|
|
346
|
+
Option.map(
|
|
347
|
+
Option.flatMap(previous.maybeHeld, (held) =>
|
|
348
|
+
moveText(
|
|
349
|
+
held,
|
|
350
|
+
previous,
|
|
351
|
+
dragAndDropUpdate.model,
|
|
352
|
+
Option.fromNullishOr(dragAndDropUpdate.outMessage),
|
|
353
|
+
putBack,
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
(text) => AnnounceMove({ text }),
|
|
364
357
|
),
|
|
365
358
|
);
|
|
366
359
|
|
|
367
|
-
return Update.withOutMessage(
|
|
368
|
-
{
|
|
369
|
-
model: nextModel,
|
|
370
|
-
commands: [...(dragAndDropUpdate.commands ?? []), ...announceMove(maybeText)],
|
|
371
|
-
},
|
|
372
|
-
dragAndDropUpdate.outMessage,
|
|
373
|
-
);
|
|
374
|
-
};
|
|
375
|
-
|
|
376
360
|
const holding = (model: Model, held: Held): Model =>
|
|
377
361
|
modifyFields(model, { maybeHeld: () => Option.some(held) });
|
|
378
362
|
|
|
379
|
-
const canLift = (model: Model): boolean => model.isMovable && phaseOf(model.dragAndDrop) === "Idle";
|
|
380
|
-
|
|
381
363
|
const liftByKeyboard = (model: Model, place: Place, columns: ReadonlyArray<Column>) => {
|
|
382
364
|
const pressed = holding(model, { card: place.card, columns, maybePressPoint: Option.none() });
|
|
383
365
|
return settle(pressed)(
|
|
@@ -545,17 +527,6 @@ export const setMovable = (model: Model, isMovable: boolean): UpdateReturn => {
|
|
|
545
527
|
|
|
546
528
|
// SUBSCRIPTION
|
|
547
529
|
|
|
548
|
-
const holdingSubscriptions = Subscription.make<Model, Message>()((entry) => ({
|
|
549
|
-
escape: entry(
|
|
550
|
-
{ isHolding: Schema.Boolean },
|
|
551
|
-
{
|
|
552
|
-
modelToDependencies: (model) => ({ isHolding: isHolding(model) }),
|
|
553
|
-
dependenciesToStream: ({ isHolding: isHoldingNow }) =>
|
|
554
|
-
isHoldingNow ? escapeKeys(Message.PressedEscape()) : Stream.empty,
|
|
555
|
-
},
|
|
556
|
-
),
|
|
557
|
-
}));
|
|
558
|
-
|
|
559
530
|
// NOTE: @foldkit/ui gap: DragAndDrop's Subscription keys are fixed, so two of its users on a
|
|
560
531
|
// page would share them. The board lifts them under its own names, behind the parent's prefix.
|
|
561
532
|
const dragSubscriptions = Subscription.lift({
|
|
@@ -567,7 +538,10 @@ const dragSubscriptions = Subscription.lift({
|
|
|
567
538
|
toParentMessage: toDragAndDropMessage,
|
|
568
539
|
});
|
|
569
540
|
|
|
570
|
-
const boardSubscriptions = Subscription.aggregate(
|
|
541
|
+
const boardSubscriptions = Subscription.aggregate(
|
|
542
|
+
escapeSubscriptions<Model, Message>(Message.PressedEscape()),
|
|
543
|
+
dragSubscriptions,
|
|
544
|
+
);
|
|
571
545
|
|
|
572
546
|
/** A held card follows the pointer, the arrows and Space move and drop it, Escape puts it
|
|
573
547
|
* back and the page scrolls near its edges, all from listeners that run only while a card is
|
|
@@ -581,9 +555,6 @@ export const subscriptions = <Prefix extends string>(keyPrefix: Prefix) =>
|
|
|
581
555
|
|
|
582
556
|
// LIFTED CARD
|
|
583
557
|
|
|
584
|
-
const pressPointOf = (model: Model): Option.Option<typeof PressPoint.Type> =>
|
|
585
|
-
Option.flatMap(model.maybeHeld, (held) => held.maybePressPoint);
|
|
586
|
-
|
|
587
558
|
/** Sizes the copy that follows the pointer to the card it lifts, and keeps the pointer where
|
|
588
559
|
* it pressed the card. Measures the card once, when the copy appears. */
|
|
589
560
|
export const SizeLiftedCard = Mount.define("SizeLiftedCard", {
|
|
@@ -627,9 +598,6 @@ export type ViewInputs = Readonly<{
|
|
|
627
598
|
toEmptyView?: (column: Column) => ReadonlyArray<Html>;
|
|
628
599
|
/** Controls at the right of a column's head, after its count ("capture here"). */
|
|
629
600
|
toColumnActions?: (column: Column) => ReadonlyArray<Html>;
|
|
630
|
-
/** Further attributes for a column's `div.board-col` (`data-status`), never a class. Wrap
|
|
631
|
-
* them with `childAttributes` so their handlers dispatch the parent's Messages. */
|
|
632
|
-
toColumnAttributes?: (column: Column) => ReadonlyArray<ChildAttribute>;
|
|
633
601
|
}>;
|
|
634
602
|
|
|
635
603
|
const INNER_CONTROL = innerControls(`[data-${TITLE_ATTRIBUTE}], [data-${GRIP_ATTRIBUTE}]`);
|
|
@@ -664,7 +632,6 @@ type PlaceInfo = Readonly<{
|
|
|
664
632
|
toCardView: (card: Card, render: CardRender) => Html;
|
|
665
633
|
toEmptyView: (column: Column) => ReadonlyArray<Html>;
|
|
666
634
|
toColumnActions: (column: Column) => ReadonlyArray<Html>;
|
|
667
|
-
toColumnAttributes: (column: Column) => ReadonlyArray<ChildAttribute>;
|
|
668
635
|
}>;
|
|
669
636
|
|
|
670
637
|
const gripHelpId = (model: Model): string => `${id(model)}-grip-help`;
|
|
@@ -872,7 +839,6 @@ const columnView = (info: PlaceInfo, column: Column, h: HtmlBuilder<Message>): H
|
|
|
872
839
|
return h.keyed("div")(
|
|
873
840
|
column.id,
|
|
874
841
|
[
|
|
875
|
-
...info.toColumnAttributes(column),
|
|
876
842
|
h.DataAttribute("droppable-id", columnHook(model, column.id)),
|
|
877
843
|
h.Class(isTarget ? "board-col drop-target" : "board-col"),
|
|
878
844
|
],
|
|
@@ -905,12 +871,11 @@ const liftedCopyView = (info: PlaceInfo, h: HtmlBuilder<Message>): ReadonlyArray
|
|
|
905
871
|
const maybeLifted = Option.all({
|
|
906
872
|
style: UiDragAndDrop.ghostStyle(model.dragAndDrop),
|
|
907
873
|
held: model.maybeHeld,
|
|
874
|
+
pressPoint: Option.flatMap(model.maybeHeld, (held) => held.maybePressPoint),
|
|
908
875
|
});
|
|
909
876
|
|
|
910
|
-
const pressPoint = Option.getOrElse(pressPointOf(model), () => ({ clientX: 0, clientY: 0 }));
|
|
911
|
-
|
|
912
877
|
return Array.fromOption(
|
|
913
|
-
Option.map(maybeLifted, ({ style, held }) =>
|
|
878
|
+
Option.map(maybeLifted, ({ style, held, pressPoint }) =>
|
|
914
879
|
h.div(
|
|
915
880
|
[
|
|
916
881
|
h.Class("board-lift"),
|
|
@@ -946,21 +911,8 @@ const liftedCopyView = (info: PlaceInfo, h: HtmlBuilder<Message>): ReadonlyArray
|
|
|
946
911
|
* follows the pointer or the keys.
|
|
947
912
|
*/
|
|
948
913
|
export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h): Html => {
|
|
949
|
-
const {
|
|
950
|
-
|
|
951
|
-
toCardView,
|
|
952
|
-
toEmptyView = () => [],
|
|
953
|
-
toColumnActions = () => [],
|
|
954
|
-
toColumnAttributes = () => [],
|
|
955
|
-
} = viewInputs;
|
|
956
|
-
const info: PlaceInfo = {
|
|
957
|
-
model,
|
|
958
|
-
columns,
|
|
959
|
-
toCardView,
|
|
960
|
-
toEmptyView,
|
|
961
|
-
toColumnActions,
|
|
962
|
-
toColumnAttributes,
|
|
963
|
-
};
|
|
914
|
+
const { columns, toCardView, toEmptyView = () => [], toColumnActions = () => [] } = viewInputs;
|
|
915
|
+
const info: PlaceInfo = { model, columns, toCardView, toEmptyView, toColumnActions };
|
|
964
916
|
|
|
965
917
|
return h.div(
|
|
966
918
|
[h.Class("board"), h.Style({ "--cols": `${columns.length}` })],
|
package/src/board/index.ts
CHANGED
|
@@ -10,10 +10,10 @@ export {
|
|
|
10
10
|
cardView,
|
|
11
11
|
id,
|
|
12
12
|
init,
|
|
13
|
-
isHolding,
|
|
14
13
|
setMovable,
|
|
15
14
|
subscriptions,
|
|
16
15
|
update,
|
|
17
16
|
view,
|
|
18
17
|
} from "./board.ts";
|
|
18
|
+
export { isHolding } from "../drag/drag.ts";
|
|
19
19
|
export type { CardRender, CardViewConfig, InitConfig, ViewInputs } from "./board.ts";
|
package/src/button/button.ts
CHANGED
|
@@ -76,7 +76,7 @@ export type ViewConfig<Message> = Look &
|
|
|
76
76
|
isDisabled?: boolean;
|
|
77
77
|
keyHint?: KeyHintConfig;
|
|
78
78
|
/** `submit` makes it its form's submit button while it is not held. */
|
|
79
|
-
type?: "button" | "submit"
|
|
79
|
+
type?: "button" | "submit";
|
|
80
80
|
/** Further attributes for the `<button>`, such as an id or a Submodel's attribute group. */
|
|
81
81
|
attributes?: Attributes<Message>;
|
|
82
82
|
}> &
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Input as UiInput } from "@foldkit/ui";
|
|
2
|
-
import { Array, Effect, HashSet, Match, Option, Schema, Stream, String, pipe } from "effect";
|
|
2
|
+
import { Array, Effect, Equal, HashSet, Match, Option, Schema, Stream, String, pipe } from "effect";
|
|
3
3
|
import * as FoldkitCommand from "foldkit/command";
|
|
4
4
|
import * as Dom from "foldkit/dom";
|
|
5
5
|
import type { Attribute, Html, HtmlBuilder, KeyboardModifiers } from "foldkit/html";
|
|
@@ -230,19 +230,10 @@ const fresh = (model: Model, maybePage: Option.Option<OpenPage>): Model =>
|
|
|
230
230
|
}),
|
|
231
231
|
);
|
|
232
232
|
|
|
233
|
-
const isSamePage = (
|
|
234
|
-
maybeFirst: Option.Option<string>,
|
|
235
|
-
maybeSecond: Option.Option<string>,
|
|
236
|
-
): boolean =>
|
|
237
|
-
Option.match(maybeFirst, {
|
|
238
|
-
onNone: () => Option.isNone(maybeSecond),
|
|
239
|
-
onSome: (commandId) => Option.contains(maybeSecond, commandId),
|
|
240
|
-
});
|
|
241
|
-
|
|
242
233
|
/** Makes the Model follow what the view showed: a page whose command is gone is dropped (with
|
|
243
234
|
* a pick out on it), so the commands' query and cursor take the reader's input again. */
|
|
244
235
|
const followShown = (model: Model, maybeShownPage: Option.Option<string>): Model =>
|
|
245
|
-
|
|
236
|
+
Equal.equals(maybePageId(model), maybeShownPage)
|
|
246
237
|
? model
|
|
247
238
|
: clearPendingKeys(
|
|
248
239
|
modifyFields(model, {
|
|
@@ -265,13 +256,12 @@ const leavePage = (model: Model): UpdateReturn =>
|
|
|
265
256
|
commands: [focusField(model), scrollPaletteToCursor(model)],
|
|
266
257
|
};
|
|
267
258
|
|
|
268
|
-
const openPage = (model: Model, commandId: string): UpdateReturn =>
|
|
269
|
-
|
|
270
|
-
model:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}));
|
|
259
|
+
const openPage = (model: Model, commandId: string): UpdateReturn => ({
|
|
260
|
+
model: clearPendingKeys(
|
|
261
|
+
modifyFields(model, { maybePage: () => Option.some(freshPage(commandId)) }),
|
|
262
|
+
),
|
|
263
|
+
commands: [focusField(model), scrollPaletteToCursor(model)],
|
|
264
|
+
});
|
|
275
265
|
|
|
276
266
|
const selectCommand = (model: Model, commandId: string): UpdateReturn =>
|
|
277
267
|
pipe(
|
|
@@ -292,18 +282,16 @@ const moveCursor = (model: Model, cursorId: string): Model =>
|
|
|
292
282
|
);
|
|
293
283
|
|
|
294
284
|
// A pick drops the keys waiting for the next render: the row picked is the one acted on.
|
|
295
|
-
const pick = (model: Model, commandId: string, choiceId: string): UpdateReturn =>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
),
|
|
302
|
-
outMessage: OutMessage.SelectedChoice({ commandId, choiceId }),
|
|
303
|
-
};
|
|
285
|
+
const pick = (model: Model, commandId: string, choiceId: string): UpdateReturn => ({
|
|
286
|
+
model: clearPendingKeys(
|
|
287
|
+
modifyFields(moveCursor(model, choiceId), { maybePicking: () => Option.some(choiceId) }),
|
|
288
|
+
),
|
|
289
|
+
outMessage: OutMessage.SelectedChoice({ commandId, choiceId }),
|
|
290
|
+
});
|
|
304
291
|
|
|
305
292
|
// What a click on a row or Enter on the cursor's does: nothing when it is held, else pick it
|
|
306
|
-
// on a page, open its page, or select it.
|
|
293
|
+
// on a page, open its page, or select it. Never while a pick is out: the click and the keys
|
|
294
|
+
// come through `unlessPicking`, and a pick clears the waiting Enter `settlePending` runs.
|
|
307
295
|
const runRow = (model: Model, row: ShownRow): UpdateReturn => {
|
|
308
296
|
if (row.isHeld) {
|
|
309
297
|
return { model };
|
|
@@ -322,12 +310,7 @@ const maybeCurrentCursor = (model: Model): Option.Option<string> =>
|
|
|
322
310
|
|
|
323
311
|
const ARROW_STEP: Readonly<Record<ArrowKey, number>> = { ArrowUp: -1, ArrowDown: 1 };
|
|
324
312
|
|
|
325
|
-
type ShownList =
|
|
326
|
-
rows: ReadonlyArray<ShownRow>;
|
|
327
|
-
maybeShownCursor: Option.Option<string>;
|
|
328
|
-
shownQuery: string;
|
|
329
|
-
shownShift: number;
|
|
330
|
-
}>;
|
|
313
|
+
type ShownList = Omit<typeof Message.PressedEnter.Type, "_tag" | "maybeShownPage">;
|
|
331
314
|
|
|
332
315
|
const isListed = (rows: ReadonlyArray<Readonly<{ id: string }>>, rowId: string): boolean =>
|
|
333
316
|
Array.some(rows, (row) => row.id === rowId);
|
|
@@ -435,7 +418,7 @@ type PendingReport = typeof Message.CompletedReportPendingKeys.Type;
|
|
|
435
418
|
const settlePending = (model: Model, report: PendingReport): UpdateReturn => {
|
|
436
419
|
const { maybeShownPage, shownQuery, shownShift, maybeRow } = report;
|
|
437
420
|
const isCurrent =
|
|
438
|
-
|
|
421
|
+
Equal.equals(maybePageId(model), maybeShownPage) &&
|
|
439
422
|
shownShift === model.cursorShift &&
|
|
440
423
|
shownQuery === query(model);
|
|
441
424
|
if (!isPending(model) || !isCurrent) {
|
|
@@ -566,7 +549,7 @@ export type ViewInputs = Readonly<{
|
|
|
566
549
|
toLiveRegionView?: (() => Html) | undefined;
|
|
567
550
|
}>;
|
|
568
551
|
|
|
569
|
-
type ShownPageState = Readonly<{
|
|
552
|
+
type ShownPageState = Readonly<{ page: Page; state: OpenPage }>;
|
|
570
553
|
|
|
571
554
|
type Shown = Readonly<{
|
|
572
555
|
maybePage: Option.Option<ShownPageState>;
|
|
@@ -580,7 +563,7 @@ const pageOf = (commands: ReadonlyArray<Command>, state: OpenPage): Option.Optio
|
|
|
580
563
|
pipe(
|
|
581
564
|
Array.findFirst(commands, (command) => command.id === state.commandId),
|
|
582
565
|
Option.flatMap((command) => Option.fromNullishOr(command.page)),
|
|
583
|
-
Option.map((page) => ({
|
|
566
|
+
Option.map((page) => ({ page, state })),
|
|
584
567
|
);
|
|
585
568
|
|
|
586
569
|
const firstListed = (
|
|
@@ -686,7 +669,7 @@ const shownQueryOf = (model: Model, shown: Shown): string =>
|
|
|
686
669
|
|
|
687
670
|
// The command whose page the view shows.
|
|
688
671
|
const shownPageIdOf = (shown: Shown): Option.Option<string> =>
|
|
689
|
-
Option.map(shown.maybePage, ({
|
|
672
|
+
Option.map(shown.maybePage, ({ state }) => state.commandId);
|
|
690
673
|
|
|
691
674
|
// NOTE: the field's keys that act on the rows are taken here, where the rows are known, as a
|
|
692
675
|
// Mount on the document cannot know them: ↑, ↓ and Enter carry what this render shows (see
|
|
@@ -24,14 +24,13 @@ const isEmptyFieldOnPage = (root: Element, field: Element): boolean =>
|
|
|
24
24
|
|
|
25
25
|
/** What a Backspace in the palette's field does: in an empty field on a page it goes back.
|
|
26
26
|
* It reads the field as it is at the press, which a render cannot know. A key from elsewhere,
|
|
27
|
-
* a key an IME is composing, a modified key
|
|
28
|
-
* while a pick is out
|
|
27
|
+
* a key an IME is composing, a modified key and a key someone already handled are left
|
|
28
|
+
* alone; while a pick is out the update refuses it. */
|
|
29
29
|
export const fieldKey = (
|
|
30
30
|
root: Element,
|
|
31
31
|
event: KeyboardEvent,
|
|
32
32
|
): Option.Option<typeof Message.PressedBackspace.Type> => {
|
|
33
33
|
const field = root.querySelector(`[data-${FIELD_ATTRIBUTE}]`);
|
|
34
|
-
const isWaiting = root.querySelector('[aria-busy="true"]') !== null;
|
|
35
34
|
if (
|
|
36
35
|
field === null ||
|
|
37
36
|
event.target !== field ||
|
|
@@ -39,7 +38,6 @@ export const fieldKey = (
|
|
|
39
38
|
event.isComposing ||
|
|
40
39
|
event.defaultPrevented ||
|
|
41
40
|
!isPlain(event) ||
|
|
42
|
-
isWaiting ||
|
|
43
41
|
!isEmptyFieldOnPage(root, field)
|
|
44
42
|
) {
|
|
45
43
|
return Option.none();
|
|
@@ -56,13 +56,15 @@ export const matches = (query: string, label: string): boolean => {
|
|
|
56
56
|
return isSubsequence(needle, String.toLowerCase(label));
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
const isBlank = (query: string): boolean => String.isEmpty(String.trim(query));
|
|
60
|
+
|
|
59
61
|
/** The commands the query finds, under their groups in the order the groups first appear,
|
|
60
62
|
* authored titles after the commands. With no query, authored titles wait. */
|
|
61
63
|
export const findCommands = (
|
|
62
64
|
commands: ReadonlyArray<Command>,
|
|
63
65
|
query: string,
|
|
64
66
|
): ReadonlyArray<Command> => {
|
|
65
|
-
const hits =
|
|
67
|
+
const hits = isBlank(query)
|
|
66
68
|
? Array.filter(commands, (command) => command.isAuthored !== true)
|
|
67
69
|
: Array.filter(commands, (command) => matches(query, command.label));
|
|
68
70
|
const ordered = [
|
|
@@ -78,8 +80,6 @@ export const findCommands = (
|
|
|
78
80
|
return Array.sort(ordered, Order.mapInput(Order.Number, groupIndex));
|
|
79
81
|
};
|
|
80
82
|
|
|
81
|
-
const isBlank = (query: string): boolean => String.isEmpty(String.trim(query));
|
|
82
|
-
|
|
83
83
|
/** The choices a page's query finds. */
|
|
84
84
|
export const findChoices = (page: Page, query: string): ReadonlyArray<Choice> =>
|
|
85
85
|
isBlank(query)
|
|
@@ -65,11 +65,13 @@ const dialogFold = foldDialog(Dialog, {
|
|
|
65
65
|
toParentOutMessage: toCancelled,
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
const FREE: Dialog.UpdateContext = { isCloseHeld: false };
|
|
69
|
-
|
|
70
68
|
/** Processes a confirm dialog Message and returns the next Model, Commands and OutMessage.
|
|
71
69
|
* While `context.isCloseHeld`, cancel, Escape and the veil change nothing. */
|
|
72
|
-
export const update = (
|
|
70
|
+
export const update = (
|
|
71
|
+
model: Model,
|
|
72
|
+
message: Message,
|
|
73
|
+
context: Dialog.UpdateContext = Dialog.FREE,
|
|
74
|
+
) =>
|
|
73
75
|
Message.match<Update.ReturnWithOutMessage<Model, Message, OutMessage>>(message, {
|
|
74
76
|
GotDialogMessage: ({ message: dialogMessage }) =>
|
|
75
77
|
dialogFold.updateWith(context)(model, dialogMessage),
|
|
@@ -10,7 +10,6 @@ import type * as Update from "foldkit/update";
|
|
|
10
10
|
|
|
11
11
|
import * as Announcer from "../announcer/announcer.ts";
|
|
12
12
|
import * as Button from "../button/button.ts";
|
|
13
|
-
import type { Attributes } from "../attributes/attributes.ts";
|
|
14
13
|
import * as IconButton from "../iconButton/iconButton.ts";
|
|
15
14
|
|
|
16
15
|
// MODEL
|
|
@@ -139,8 +138,6 @@ export type ViewInputs = Readonly<{
|
|
|
139
138
|
* word (`"copy"`). */
|
|
140
139
|
label: string;
|
|
141
140
|
variant?: Variant;
|
|
142
|
-
/** Further attributes for the `<button>`, never a class. */
|
|
143
|
-
attributes?: Attributes<Message>;
|
|
144
141
|
}>;
|
|
145
142
|
|
|
146
143
|
/**
|
|
@@ -151,9 +148,9 @@ export type ViewInputs = Readonly<{
|
|
|
151
148
|
* resources.
|
|
152
149
|
*/
|
|
153
150
|
export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h): Html => {
|
|
154
|
-
const { text, label, variant = "Icon"
|
|
151
|
+
const { text, label, variant = "Icon" } = viewInputs;
|
|
155
152
|
const isDone = isCopied(model);
|
|
156
|
-
const buttonAttributes = [
|
|
153
|
+
const buttonAttributes = [h.Id(id(model))];
|
|
157
154
|
const onClick = Message.ClickedCopy({ text });
|
|
158
155
|
|
|
159
156
|
return variant === "Icon"
|
package/src/crumbs/crumbs.ts
CHANGED
|
@@ -14,8 +14,6 @@ export type Crumb = Readonly<{
|
|
|
14
14
|
/** Configuration for rendering crumbs with `view`. */
|
|
15
15
|
export type ViewConfig = Readonly<{
|
|
16
16
|
crumbs: ReadonlyArray<Crumb>;
|
|
17
|
-
/** Names the navigation for screen readers. */
|
|
18
|
-
label?: string;
|
|
19
17
|
}>;
|
|
20
18
|
|
|
21
19
|
const crumbView =
|
|
@@ -34,10 +32,10 @@ const crumbView =
|
|
|
34
32
|
/** The path to where you are, in the pixel face, split by muted slashes: a navigation
|
|
35
33
|
* landmark named "Breadcrumb", its steps an ordered list, the last `aria-current="page"`. */
|
|
36
34
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
37
|
-
const { crumbs
|
|
35
|
+
const { crumbs } = config;
|
|
38
36
|
|
|
39
37
|
return h.nav(
|
|
40
|
-
[h.AriaLabel(
|
|
38
|
+
[h.AriaLabel("Breadcrumb"), h.Class("crumbs")],
|
|
41
39
|
[h.ol([], Array.map(crumbs, crumbView(crumbs.length - 1, h)))],
|
|
42
40
|
);
|
|
43
41
|
};
|
package/src/dialog/dialog.ts
CHANGED
|
@@ -89,7 +89,8 @@ export type UpdateContext = Readonly<{
|
|
|
89
89
|
isCloseHeld: boolean;
|
|
90
90
|
}>;
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
/** The context of a dialog whose close is not held, the default. */
|
|
93
|
+
export const FREE: UpdateContext = { isCloseHeld: false };
|
|
93
94
|
|
|
94
95
|
// NOTE: @foldkit/ui gap: Dialog has no way to hold a close. Escape, a press on the veil and a
|
|
95
96
|
// control given the `closeButton` group all send `RequestedClose`, so a held dialog reads it
|
|
@@ -248,7 +249,7 @@ export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h
|
|
|
248
249
|
} = viewInputs;
|
|
249
250
|
|
|
250
251
|
const actionBar = childAttributes([
|
|
251
|
-
h.
|
|
252
|
+
h.Attribute(ACTION_BAR_ATTRIBUTE, ""),
|
|
252
253
|
h.OnMount(BindActionKeys()),
|
|
253
254
|
]);
|
|
254
255
|
|
package/src/drag/drag.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Shared by Board and Sortable, the two users of @foldkit/ui's DragAndDrop, and Reel, which
|
|
2
|
-
// holds its own: the phase a drag is in,
|
|
3
|
-
// lifts, Escape taken before anything else, the
|
|
4
|
-
// measure, and the closest-centre drop target a
|
|
5
|
-
// handlers are the Reel's and the Rail's.
|
|
2
|
+
// holds its own: the phase a drag is in, what a hold keeps and how a step settles it, the
|
|
3
|
+
// DragAndDrop Messages both build, what a press lifts, Escape taken before anything else, the
|
|
4
|
+
// pressing pointer's events, the lifted copy's measure, and the closest-centre drop target a
|
|
5
|
+
// sortable resolves for a pointer. A splitter's handlers are the Reel's and the Rail's.
|
|
6
6
|
|
|
7
7
|
import { DragAndDrop as UiDragAndDrop } from "@foldkit/ui";
|
|
8
|
-
import { Array, Effect, Option, Order, Stream, pipe } from "effect";
|
|
8
|
+
import { Array, Effect, Option, Order, Schema, Stream, pipe } from "effect";
|
|
9
|
+
import type * as Command from "foldkit/command";
|
|
9
10
|
import * as Dom from "foldkit/dom";
|
|
10
11
|
import type { Attribute, HtmlBuilder } from "foldkit/html";
|
|
12
|
+
import * as Subscription from "foldkit/subscription";
|
|
13
|
+
import * as Update from "foldkit/update";
|
|
11
14
|
|
|
12
15
|
import { isPlain } from "../keys/keys.ts";
|
|
13
16
|
|
|
@@ -36,6 +39,38 @@ export const phaseOf = (dragAndDrop: UiDragAndDrop.Model): Phase => {
|
|
|
36
39
|
export const isCarrying = (phase: Phase): boolean =>
|
|
37
40
|
phase === "PointerDragging" || phase === "KeyboardDragging";
|
|
38
41
|
|
|
42
|
+
// HOLD
|
|
43
|
+
|
|
44
|
+
/** What a Board's and a Sortable's Model share: the DragAndDrop, whether items may move, and
|
|
45
|
+
* what the list keeps while an item is pressed or held. */
|
|
46
|
+
type Holder = Readonly<{
|
|
47
|
+
dragAndDrop: UiDragAndDrop.Model;
|
|
48
|
+
isMovable: boolean;
|
|
49
|
+
maybeHeld: Option.Option<unknown>;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
/** Whether an item is pressed or held. While it is, the arrow keys, Space and Escape belong to
|
|
53
|
+
* the list: pause the page's own bindings for them. The list takes Escape before anything
|
|
54
|
+
* else sees it. */
|
|
55
|
+
export const isHolding = (model: Holder): boolean => phaseOf(model.dragAndDrop) !== "Idle";
|
|
56
|
+
|
|
57
|
+
/** Whether a press may lift an item now: items may move and none is pressed or held. */
|
|
58
|
+
export const canLift = (model: Holder): boolean => model.isMovable && !isHolding(model);
|
|
59
|
+
|
|
60
|
+
/** Settles a DragAndDrop step: forgets the held item once it is down, and runs what the step
|
|
61
|
+
* says (`maybeAnnouncement`) after the step's own Commands. */
|
|
62
|
+
export const settle = <Model extends Holder, Message, OutMessage>(
|
|
63
|
+
step: Update.ReturnWithOutMessage<Model, Message, OutMessage>,
|
|
64
|
+
maybeAnnouncement: Option.Option<Command.Command<Message>>,
|
|
65
|
+
): Update.ReturnWithOutMessage<Model, Message, OutMessage> =>
|
|
66
|
+
Update.withOutMessage(
|
|
67
|
+
{
|
|
68
|
+
model: isHolding(step.model) ? step.model : { ...step.model, maybeHeld: Option.none() },
|
|
69
|
+
commands: [...(step.commands ?? []), ...Array.fromOption(maybeAnnouncement)],
|
|
70
|
+
},
|
|
71
|
+
step.outMessage,
|
|
72
|
+
);
|
|
73
|
+
|
|
39
74
|
// CHILD MESSAGES
|
|
40
75
|
|
|
41
76
|
// NOTE: @foldkit/ui gap: DragAndDrop exposes no update capability for a press, a keyboard
|
|
@@ -114,6 +149,20 @@ export const escapeKeys = <Message>(message: Message): Stream.Stream<Message> =>
|
|
|
114
149
|
options: { capture: true },
|
|
115
150
|
});
|
|
116
151
|
|
|
152
|
+
/** A list's `escape` Subscription: `escapeKeys` as `message` while an item is pressed or
|
|
153
|
+
* held. */
|
|
154
|
+
export const escapeSubscriptions = <Model extends Holder, Message>(message: Message) =>
|
|
155
|
+
Subscription.make<Model, Message>()((entry) => ({
|
|
156
|
+
escape: entry(
|
|
157
|
+
{ isHolding: Schema.Boolean },
|
|
158
|
+
{
|
|
159
|
+
modelToDependencies: (model) => ({ isHolding: isHolding(model) }),
|
|
160
|
+
dependenciesToStream: ({ isHolding: isHoldingNow }) =>
|
|
161
|
+
isHoldingNow ? escapeKeys(message) : Stream.empty,
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
}));
|
|
165
|
+
|
|
117
166
|
// POINTER
|
|
118
167
|
|
|
119
168
|
/** The moves, release and cancel of the pointer `pointerId`, on the document; another
|