@weasel-js/gestures 1.10.0 → 1.10.1
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/README.md +106 -4
- package/dist/index.d.ts +59 -24
- package/dist/index.js +131 -57
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# @weasel-js/gestures
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The vocabulary weasel uses to say which user input fires what, and the pure
|
|
4
|
+
functions that decide whether an input matches. It has no React, no scene, and
|
|
5
|
+
no dependencies, so it can run in a test, a worker, or a host that is not
|
|
6
|
+
weasel at all.
|
|
4
7
|
|
|
5
8
|
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
6
9
|
scene-graph canvas kit for React. See the
|
|
7
|
-
[API reference](https://orochi235.github.io/weasel/
|
|
10
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/gestures).
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
@@ -12,12 +15,111 @@ scene-graph canvas kit for React. See the
|
|
|
12
15
|
npm install @weasel-js/gestures
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
##
|
|
18
|
+
## Terms
|
|
19
|
+
|
|
20
|
+
These words have fixed meanings across weasel:
|
|
21
|
+
|
|
22
|
+
- A **gesture** is a form of input: a click, a drag, a wheel turn, a keystroke,
|
|
23
|
+
a pinch, a file dropped on the canvas.
|
|
24
|
+
- An **action** is a user-intent operation: move, delete, zoom in.
|
|
25
|
+
- An **interaction** is a gesture bound to an action: "shift-click on empty
|
|
26
|
+
canvas adds to the selection".
|
|
27
|
+
|
|
28
|
+
This package covers the gesture side only. It describes gestures, recognizes
|
|
29
|
+
them from raw pointer input, and matches them against descriptions. Actions,
|
|
30
|
+
and the dispatcher that runs the action a matched gesture is bound to, live in
|
|
31
|
+
other weasel packages.
|
|
32
|
+
|
|
33
|
+
## What is in it
|
|
34
|
+
|
|
35
|
+
### Describing a gesture
|
|
36
|
+
|
|
37
|
+
A `GestureSpec` is an object describing one gesture a binding listens for:
|
|
38
|
+
its `kind` (`'click'`, `'drag'`, `'key'`, `'wheel'`, and so on), and optionally
|
|
39
|
+
the modifier keys that must be held, what the pointer must be over, and the
|
|
40
|
+
phase the owning tool must be in.
|
|
41
|
+
|
|
42
|
+
A **route** is the same description as a string, short enough to write inline
|
|
43
|
+
and to show in a shortcut list: `[initial] click => empty +shift`.
|
|
44
|
+
|
|
45
|
+
`parseRoute` turns a route into a `ParsedRoute`, `formatRoute` writes the
|
|
46
|
+
canonical string back, `routeToSpec` converts a parsed route into the
|
|
47
|
+
`GestureSpec` the matcher takes, and `describeRoute` renders it as an English
|
|
48
|
+
sentence. `GESTURE_DESCRIPTORS` is the table of gesture names the grammar
|
|
49
|
+
accepts, with whether each takes a target and what argument it carries.
|
|
50
|
+
|
|
51
|
+
### Matching input against a description
|
|
52
|
+
|
|
53
|
+
An `InputEvent` is one normalized input: a union keyed by `kind`, each arm
|
|
54
|
+
carrying the modifier state and whatever that kind of input reports.
|
|
55
|
+
|
|
56
|
+
`matchSpec(event, spec, isMac)` returns whether an event satisfies a spec.
|
|
57
|
+
Its parts are exported too: `matchModifiers`, `matchKey`, `matchTarget`, and
|
|
58
|
+
`matchPhase`. When several specs match one event, `specificity(spec)` gives a
|
|
59
|
+
tuple to rank them by, so the narrower description wins.
|
|
60
|
+
|
|
61
|
+
### Recognizing gestures from pointer input
|
|
62
|
+
|
|
63
|
+
`createPointerRecognizer` turns a stream of pointer presses, moves, and
|
|
64
|
+
releases into `InputEvent`s. It decides when a press has moved far enough to be
|
|
65
|
+
a drag, when a release in place is a click, when two clicks make a double
|
|
66
|
+
click, when a held touch is a long-press, and when several pointers form a
|
|
67
|
+
multitouch gesture. It touches no DOM: the host feeds it samples and supplies
|
|
68
|
+
the clock, and it hands each recognized event to a sink the host provides.
|
|
69
|
+
|
|
70
|
+
The thresholds it uses are exported as constants: `DRAG_THRESHOLD_PX`,
|
|
71
|
+
`LONG_PRESS_MS`, `DOUBLE_CLICK_MAX_MS`, `DOUBLE_CLICK_MAX_PX`, and
|
|
72
|
+
`TAP_THRESHOLD_PX`.
|
|
73
|
+
|
|
74
|
+
### How they fit together
|
|
75
|
+
|
|
76
|
+
| From | Through | To |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Pointer samples | `createPointerRecognizer` | `InputEvent` |
|
|
79
|
+
| Route string | `parseRoute`, then `routeToSpec` | `GestureSpec` |
|
|
80
|
+
| `InputEvent` and `GestureSpec` | `matchSpec` | `true` or `false` |
|
|
81
|
+
|
|
82
|
+
A host listens to the DOM, feeds pointer samples to a recognizer, and builds
|
|
83
|
+
key, wheel, and drop events itself. Each binding carries a `GestureSpec`,
|
|
84
|
+
written directly or parsed from a route. For every event, the host asks
|
|
85
|
+
`matchSpec` which bindings apply, ranks them with `specificity`, and runs the
|
|
86
|
+
winner's action.
|
|
87
|
+
|
|
88
|
+
The package also exports a few helpers a host needs while doing this:
|
|
89
|
+
`isApplePlatform` to decide what the `mod` modifier means, and
|
|
90
|
+
`isEditableTarget`, `focusedControlOwnsKey`, and `isInControlWithin` to leave
|
|
91
|
+
keys and presses alone when they belong to a text field or a button.
|
|
92
|
+
|
|
93
|
+
## Example
|
|
94
|
+
|
|
95
|
+
Parse a route, turn it into a spec, and test an event against it:
|
|
16
96
|
|
|
17
97
|
```ts
|
|
18
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
describeRoute, matchSpec, parseRoute, routeToSpec, type InputEvent,
|
|
100
|
+
} from '@weasel-js/gestures';
|
|
101
|
+
|
|
102
|
+
const route = parseRoute('[*:*] click => empty +shift');
|
|
103
|
+
const spec = routeToSpec(route);
|
|
104
|
+
// { kind: 'click', target: 'empty', mods: { shift: true } }
|
|
105
|
+
|
|
106
|
+
const shiftClick: InputEvent = {
|
|
107
|
+
kind: 'click',
|
|
108
|
+
bodyTarget: 'empty',
|
|
109
|
+
altKey: false, ctrlKey: false, metaKey: false, shiftKey: true,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
matchSpec(shiftClick, spec, false); // true
|
|
113
|
+
matchSpec({ ...shiftClick, shiftKey: false }, spec, false); // false: shift is required
|
|
114
|
+
|
|
115
|
+
describeRoute(parseRoute('[initial] drag +mod ?shift'));
|
|
116
|
+
// 'Fires when the user Mod-drags anywhere, while the tool is idle (Shift optional).'
|
|
19
117
|
```
|
|
20
118
|
|
|
119
|
+
Modifier matching is strict. A modifier the spec does not mention must not be
|
|
120
|
+
held, so a spec for plain `click` does not match a shift-click. Mark a modifier
|
|
121
|
+
`?shift` in a route, or `shift: 'optional'` in a spec, to accept it either way.
|
|
122
|
+
|
|
21
123
|
## License
|
|
22
124
|
|
|
23
125
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -338,6 +338,11 @@ interface PointerDownSpec {
|
|
|
338
338
|
* The pointer moving over the surface with no button pressed. Fires on every
|
|
339
339
|
* such move; a held pointer's moves belong to its drag and fire nothing here.
|
|
340
340
|
*
|
|
341
|
+
* With `motion: 'leave'` it fires instead when the pointer passes out of what
|
|
342
|
+
* the binding targets: onto something its `target` does not match, or off the
|
|
343
|
+
* surface. With no `target` that is the whole surface. Its params are the
|
|
344
|
+
* move's that took the pointer out, or the last move's before it left the surface.
|
|
345
|
+
*
|
|
341
346
|
* A hover binding claims nothing. Every live binding that matches the move
|
|
342
347
|
* runs, not only the best one, and the move still reaches the hover-cursor
|
|
343
348
|
* pump. There is no press to own, so bind an immediate action, not an ongoing
|
|
@@ -346,6 +351,7 @@ interface PointerDownSpec {
|
|
|
346
351
|
*/
|
|
347
352
|
interface HoverSpec {
|
|
348
353
|
kind: 'hover';
|
|
354
|
+
motion?: 'move' | 'leave';
|
|
349
355
|
target?: TargetSpec;
|
|
350
356
|
mods?: ModSpec;
|
|
351
357
|
phase?: PhaseSpec;
|
|
@@ -741,9 +747,19 @@ interface ContextMenuEvent extends EventModifiers {
|
|
|
741
747
|
bodyTarget?: BodyTarget;
|
|
742
748
|
bodyKind?: BodyKind;
|
|
743
749
|
}
|
|
744
|
-
/**
|
|
750
|
+
/** What a hover found under the pointer. */
|
|
751
|
+
interface HoverWhere {
|
|
752
|
+
affordance?: unknown;
|
|
753
|
+
bodyTarget?: BodyTarget;
|
|
754
|
+
bodyKind?: BodyKind;
|
|
755
|
+
}
|
|
756
|
+
/** A move of a pointer with no button pressed, or its leaving the surface. Matched by `hover` specs. */
|
|
745
757
|
interface HoverEvent extends EventModifiers {
|
|
746
758
|
kind: 'hover';
|
|
759
|
+
/** What the pointer was over at its last move, when it has made one since coming onto the surface. */
|
|
760
|
+
from?: HoverWhere;
|
|
761
|
+
/** The pointer has left the surface. Where it is and what it is over are its last move's. */
|
|
762
|
+
off?: boolean;
|
|
747
763
|
/** World-space position of the pointer. */
|
|
748
764
|
x: number;
|
|
749
765
|
y: number;
|
|
@@ -856,29 +872,8 @@ type InputEvent = KeyEvent | KeyHeldEvent | WheelEvent | PinchEvent | PointerDow
|
|
|
856
872
|
*/
|
|
857
873
|
declare function stylusOf(src: PointerSampleStylus): PointerSampleStylus;
|
|
858
874
|
|
|
859
|
-
/**
|
|
875
|
+
/** Target matching: a `TargetSpec` against what an event classified under its point. */
|
|
860
876
|
|
|
861
|
-
/** The modifier-state fields `matchModifiers` reads. Structural, so a DOM
|
|
862
|
-
* `KeyboardEvent` or `PointerEvent` satisfies it directly. */
|
|
863
|
-
type ModifiersEvent = {
|
|
864
|
-
altKey: boolean;
|
|
865
|
-
ctrlKey: boolean;
|
|
866
|
-
metaKey: boolean;
|
|
867
|
-
shiftKey: boolean;
|
|
868
|
-
};
|
|
869
|
-
/**
|
|
870
|
-
* Strict modifier match.
|
|
871
|
-
*
|
|
872
|
-
* - Omitted or `false`: MUST NOT be held.
|
|
873
|
-
* - `true`: MUST be held.
|
|
874
|
-
* - `'optional'`: accepts either held or unheld.
|
|
875
|
-
* - `mod` is platform-aware: matches `metaKey` on mac, `ctrlKey` elsewhere,
|
|
876
|
-
* and the *other* platform key is forbidden. Callers should not combine
|
|
877
|
-
* `mod` with `meta`/`ctrl`.
|
|
878
|
-
*/
|
|
879
|
-
declare function matchModifiers(e: ModifiersEvent, mods: ModSpec | undefined, isMac: boolean): boolean;
|
|
880
|
-
/** Case-insensitive key match; supports string or string[] (any-of). */
|
|
881
|
-
declare function matchKey(eventKey: string, specKey: string | string[]): boolean;
|
|
882
877
|
/**
|
|
883
878
|
* A {@link TargetSpec} resolved to its form, with the parts each form encodes
|
|
884
879
|
* in its string pulled out. This is the one enumeration of the target forms:
|
|
@@ -932,6 +927,30 @@ declare function parseTargetSpec(spec: TargetSpec): TargetSpecForm | null;
|
|
|
932
927
|
* silent wildcard.
|
|
933
928
|
*/
|
|
934
929
|
declare function matchTarget(target: unknown, specTarget: TargetSpec | undefined, bodyTarget?: string, bodyKind?: string): boolean;
|
|
930
|
+
|
|
931
|
+
/** Pure gesture matcher — no React, no state, no DOM. */
|
|
932
|
+
|
|
933
|
+
/** The modifier-state fields `matchModifiers` reads. Structural, so a DOM
|
|
934
|
+
* `KeyboardEvent` or `PointerEvent` satisfies it directly. */
|
|
935
|
+
type ModifiersEvent = {
|
|
936
|
+
altKey: boolean;
|
|
937
|
+
ctrlKey: boolean;
|
|
938
|
+
metaKey: boolean;
|
|
939
|
+
shiftKey: boolean;
|
|
940
|
+
};
|
|
941
|
+
/**
|
|
942
|
+
* Strict modifier match.
|
|
943
|
+
*
|
|
944
|
+
* - Omitted or `false`: MUST NOT be held.
|
|
945
|
+
* - `true`: MUST be held.
|
|
946
|
+
* - `'optional'`: accepts either held or unheld.
|
|
947
|
+
* - `mod` is platform-aware: matches `metaKey` on mac, `ctrlKey` elsewhere,
|
|
948
|
+
* and the *other* platform key is forbidden. Callers should not combine
|
|
949
|
+
* `mod` with `meta`/`ctrl`.
|
|
950
|
+
*/
|
|
951
|
+
declare function matchModifiers(e: ModifiersEvent, mods: ModSpec | undefined, isMac: boolean): boolean;
|
|
952
|
+
/** Case-insensitive key match; supports string or string[] (any-of). */
|
|
953
|
+
declare function matchKey(eventKey: string, specKey: string | string[]): boolean;
|
|
935
954
|
/** Per-tool gesture-lifecycle state at match time. `selfChannel` is the tool
|
|
936
955
|
* id `'&'` resolves to — the tool that owns the binding being evaluated — or
|
|
937
956
|
* `null` for a binding without an owning tool, in which case no `'&'` atom
|
|
@@ -1250,6 +1269,22 @@ declare const CONTROL_SELECTOR: string;
|
|
|
1250
1269
|
*/
|
|
1251
1270
|
declare function isInControlWithin(target: EventTarget | null, host: Element): boolean;
|
|
1252
1271
|
|
|
1272
|
+
/**
|
|
1273
|
+
* Which pointers are down on an element: pressed on it or inside it, with any
|
|
1274
|
+
* button, and not yet released. The one answer to "is this move a hover" for
|
|
1275
|
+
* everything listening on that element.
|
|
1276
|
+
*/
|
|
1277
|
+
interface HeldPointers {
|
|
1278
|
+
count(): number;
|
|
1279
|
+
/** Stop reading. The element's listeners go with its last reader. */
|
|
1280
|
+
release(): void;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Read the pointers held on `element`. Every reader of one element shares one
|
|
1284
|
+
* set of listeners, so they cannot disagree.
|
|
1285
|
+
*/
|
|
1286
|
+
declare function heldPointers(element: Element): HeldPointers;
|
|
1287
|
+
|
|
1253
1288
|
/** Returns true when the target takes typed text: an input, a textarea, or a contenteditable element. */
|
|
1254
1289
|
declare function isEditableTarget(target: EventTarget | null): boolean;
|
|
1255
1290
|
/**
|
|
@@ -1268,4 +1303,4 @@ declare function activatesFocusedControl(e: KeyboardEvent): boolean;
|
|
|
1268
1303
|
*/
|
|
1269
1304
|
declare function focusedControlOwnsKey(e: KeyboardEvent): boolean;
|
|
1270
1305
|
|
|
1271
|
-
export { ACTIVE_SIGILS, type BodyClassification, type BodyKind, type BodyTarget, CONTROL_SELECTOR, type ChannelRef, type ClaimableGesture, type ClickEvent, type ClickMemory, type ClickObserver, type ClickSpec, type ContextMenuEvent, type ContextMenuSpec, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, type DescribeRouteOptions, type DoubleClickEvent, type DoubleClickSpec, type DragSpec, type DropEvent, type DropSpec, type EventModifiers, type FixedSeat, GESTURE_DESCRIPTORS, type GestureArgSpec, type GestureDescriptor, type GestureName, type GestureSpec, type HeldPress, type HoverEvent, type HoverSpec, type IngestItem, type InputEvent, type KeyEvent, type KeyHeldEvent, type KeyHeldSpec, type KeySpec, LONG_PRESS_MS, type LongPressEvent, type LongPressSpec, MOD_NAME_SET, type ModRequirement, type ModSpec, type ModifierCombo, type ModifierKey, type ModifiersEvent, type MultiTouchEvent, type MultiTouchGeometry, type MultiTouchSpec, type MultiTouchTapEvent, type MultiTouchTapSpec, type OptionalMod, type ParsedKeyRoute, type ParsedModifiers, type ParsedRoute, type PasteEvent, type PasteSpec, type PhaseAtom, type PhaseContext, type PhaseSpec, type PinchEvent, type PinchSpec, type Point, type PointerCancelEvent, type PointerDownEvent, type PointerDownSpec, type PointerIdentity, type PointerMoveEvent, type PointerRecognizer, type PointerRecognizerOptions, type PointerSample, type PointerSampleStylus, type PointerUpEvent, type PressFacts, type PressSample, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, type RecognizerClock, type RecognizerObserver, type RecognizerSink, type ReleaseFacts, type ReleaseOutcome, type RouteDescriptionPart, type RouteFieldName, type RoutePhase, type RouteTermLabel, TAP_THRESHOLD_PX, type TargetPredicate, type TargetSpec, type TargetSpecForm, UNKNOWN_MIME, VALID_MOD_NAMES, type WheelEvent, type WheelSpec, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
|
|
1306
|
+
export { ACTIVE_SIGILS, type BodyClassification, type BodyKind, type BodyTarget, CONTROL_SELECTOR, type ChannelRef, type ClaimableGesture, type ClickEvent, type ClickMemory, type ClickObserver, type ClickSpec, type ContextMenuEvent, type ContextMenuSpec, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, type DescribeRouteOptions, type DoubleClickEvent, type DoubleClickSpec, type DragSpec, type DropEvent, type DropSpec, type EventModifiers, type FixedSeat, GESTURE_DESCRIPTORS, type GestureArgSpec, type GestureDescriptor, type GestureName, type GestureSpec, type HeldPointers, type HeldPress, type HoverEvent, type HoverSpec, type HoverWhere, type IngestItem, type InputEvent, type KeyEvent, type KeyHeldEvent, type KeyHeldSpec, type KeySpec, LONG_PRESS_MS, type LongPressEvent, type LongPressSpec, MOD_NAME_SET, type ModRequirement, type ModSpec, type ModifierCombo, type ModifierKey, type ModifiersEvent, type MultiTouchEvent, type MultiTouchGeometry, type MultiTouchSpec, type MultiTouchTapEvent, type MultiTouchTapSpec, type OptionalMod, type ParsedKeyRoute, type ParsedModifiers, type ParsedRoute, type PasteEvent, type PasteSpec, type PhaseAtom, type PhaseContext, type PhaseSpec, type PinchEvent, type PinchSpec, type Point, type PointerCancelEvent, type PointerDownEvent, type PointerDownSpec, type PointerIdentity, type PointerMoveEvent, type PointerRecognizer, type PointerRecognizerOptions, type PointerSample, type PointerSampleStylus, type PointerUpEvent, type PressFacts, type PressSample, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, type RecognizerClock, type RecognizerObserver, type RecognizerSink, type ReleaseFacts, type ReleaseOutcome, type RouteDescriptionPart, type RouteFieldName, type RoutePhase, type RouteTermLabel, TAP_THRESHOLD_PX, type TargetPredicate, type TargetSpec, type TargetSpecForm, UNKNOWN_MIME, VALID_MOD_NAMES, type WheelEvent, type WheelSpec, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var GESTURE_DESCRIPTORS = [
|
|
3
3
|
{ name: "click", hasTarget: true },
|
|
4
4
|
{ name: "pointerDown", hasTarget: true },
|
|
5
|
-
{ name: "hover", hasTarget: true },
|
|
5
|
+
{ name: "hover", hasTarget: true, arg: { name: "motion", values: ["move", "leave"], default: "move" } },
|
|
6
6
|
{ name: "dblTap", hasTarget: true },
|
|
7
7
|
{ name: "drag", hasTarget: true },
|
|
8
8
|
{ name: "wheel", hasTarget: true, arg: { name: "direction", values: ["up", "down", "*"], default: "*" } },
|
|
@@ -286,47 +286,7 @@ function matchIngestTypes(items, types) {
|
|
|
286
286
|
return items.some((it) => it.mime === UNKNOWN_MIME || types.some((g) => mimeMatchesGlob(it.mime, g)));
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
// src/ui/
|
|
290
|
-
function resolveSpecValue(value) {
|
|
291
|
-
if (value === true) return "required";
|
|
292
|
-
if (value === "optional") return "optional";
|
|
293
|
-
return "forbidden";
|
|
294
|
-
}
|
|
295
|
-
function matchModifiers(e, mods2, isMac) {
|
|
296
|
-
let alt = "forbidden";
|
|
297
|
-
let ctrl = "forbidden";
|
|
298
|
-
let meta = "forbidden";
|
|
299
|
-
let shift = "forbidden";
|
|
300
|
-
if (mods2) {
|
|
301
|
-
alt = resolveSpecValue(mods2.alt);
|
|
302
|
-
shift = resolveSpecValue(mods2.shift);
|
|
303
|
-
if (mods2.mod !== void 0) {
|
|
304
|
-
const modReq = resolveSpecValue(mods2.mod);
|
|
305
|
-
if (isMac) meta = modReq;
|
|
306
|
-
else ctrl = modReq;
|
|
307
|
-
} else {
|
|
308
|
-
meta = resolveSpecValue(mods2.meta);
|
|
309
|
-
ctrl = resolveSpecValue(mods2.ctrl);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return checkKey(alt, e.altKey) && checkKey(ctrl, e.ctrlKey) && checkKey(meta, e.metaKey) && checkKey(shift, e.shiftKey);
|
|
313
|
-
}
|
|
314
|
-
function checkKey(req, held) {
|
|
315
|
-
if (req === "required") return held;
|
|
316
|
-
if (req === "forbidden") return !held;
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
function matchKey(eventKey, specKey) {
|
|
320
|
-
const lower = eventKey.toLowerCase();
|
|
321
|
-
if (Array.isArray(specKey)) {
|
|
322
|
-
return specKey.some((k) => k.toLowerCase() === lower);
|
|
323
|
-
}
|
|
324
|
-
return specKey.toLowerCase() === lower;
|
|
325
|
-
}
|
|
326
|
-
function matchKeyOrCode(e, spec) {
|
|
327
|
-
if (spec.code !== void 0 && e.code !== void 0 && [spec.code].flat().includes(e.code)) return true;
|
|
328
|
-
return matchKey(e.key, spec.key);
|
|
329
|
-
}
|
|
289
|
+
// src/ui/matchTarget.ts
|
|
330
290
|
function affordanceKindOf(target) {
|
|
331
291
|
if (typeof target !== "object" || target === null) return void 0;
|
|
332
292
|
const k = target.kind;
|
|
@@ -379,6 +339,48 @@ function matchTarget(target, specTarget, bodyTarget, bodyKind) {
|
|
|
379
339
|
}
|
|
380
340
|
}
|
|
381
341
|
}
|
|
342
|
+
|
|
343
|
+
// src/ui/match.ts
|
|
344
|
+
function resolveSpecValue(value) {
|
|
345
|
+
if (value === true) return "required";
|
|
346
|
+
if (value === "optional") return "optional";
|
|
347
|
+
return "forbidden";
|
|
348
|
+
}
|
|
349
|
+
function matchModifiers(e, mods2, isMac) {
|
|
350
|
+
let alt = "forbidden";
|
|
351
|
+
let ctrl = "forbidden";
|
|
352
|
+
let meta = "forbidden";
|
|
353
|
+
let shift = "forbidden";
|
|
354
|
+
if (mods2) {
|
|
355
|
+
alt = resolveSpecValue(mods2.alt);
|
|
356
|
+
shift = resolveSpecValue(mods2.shift);
|
|
357
|
+
if (mods2.mod !== void 0) {
|
|
358
|
+
const modReq = resolveSpecValue(mods2.mod);
|
|
359
|
+
if (isMac) meta = modReq;
|
|
360
|
+
else ctrl = modReq;
|
|
361
|
+
} else {
|
|
362
|
+
meta = resolveSpecValue(mods2.meta);
|
|
363
|
+
ctrl = resolveSpecValue(mods2.ctrl);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return checkKey(alt, e.altKey) && checkKey(ctrl, e.ctrlKey) && checkKey(meta, e.metaKey) && checkKey(shift, e.shiftKey);
|
|
367
|
+
}
|
|
368
|
+
function checkKey(req, held) {
|
|
369
|
+
if (req === "required") return held;
|
|
370
|
+
if (req === "forbidden") return !held;
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
function matchKey(eventKey, specKey) {
|
|
374
|
+
const lower = eventKey.toLowerCase();
|
|
375
|
+
if (Array.isArray(specKey)) {
|
|
376
|
+
return specKey.some((k) => k.toLowerCase() === lower);
|
|
377
|
+
}
|
|
378
|
+
return specKey.toLowerCase() === lower;
|
|
379
|
+
}
|
|
380
|
+
function matchKeyOrCode(e, spec) {
|
|
381
|
+
if (spec.code !== void 0 && e.code !== void 0 && [spec.code].flat().includes(e.code)) return true;
|
|
382
|
+
return matchKey(e.key, spec.key);
|
|
383
|
+
}
|
|
382
384
|
function normalizePhase(spec) {
|
|
383
385
|
if (typeof spec === "string") {
|
|
384
386
|
return [{ channel: "&", phase: spec }];
|
|
@@ -406,6 +408,16 @@ function matchPhaseAtom(a, ctx) {
|
|
|
406
408
|
const isEngaged = ctx.engagedChannels.has(id);
|
|
407
409
|
return a.phase === "engaged" ? isEngaged : !isEngaged;
|
|
408
410
|
}
|
|
411
|
+
function matchPress(e, spec, isMac) {
|
|
412
|
+
if (e.kind !== "pointerdown") return false;
|
|
413
|
+
if (spec.kind === "drag") {
|
|
414
|
+
if (e.stage === "press") return false;
|
|
415
|
+
} else if (e.stage !== "press") {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
if (!matchModifiers(e, spec.mods, isMac)) return false;
|
|
419
|
+
return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
|
|
420
|
+
}
|
|
409
421
|
function matchSpec(e, spec, isMac, phaseCtx) {
|
|
410
422
|
if (spec.phase !== void 0) {
|
|
411
423
|
const ctx = phaseCtx ?? { selfChannel: null, engagedChannels: EMPTY_ENGAGED };
|
|
@@ -452,24 +464,22 @@ function matchSpec(e, spec, isMac, phaseCtx) {
|
|
|
452
464
|
// Each happens at a point, so its target is what the event classified
|
|
453
465
|
// there: for a long-press, the affordance its press landed on.
|
|
454
466
|
case "contextMenu":
|
|
455
|
-
case "longPress":
|
|
456
|
-
|
|
457
|
-
if (e.kind !== "contextmenu" && e.kind !== "longpress" && e.kind !== "hover") return false;
|
|
467
|
+
case "longPress": {
|
|
468
|
+
if (e.kind !== "contextmenu" && e.kind !== "longpress") return false;
|
|
458
469
|
if (e.kind !== spec.kind.toLowerCase()) return false;
|
|
459
470
|
if (!matchModifiers(e, spec.mods, isMac)) return false;
|
|
460
471
|
return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
|
|
461
472
|
}
|
|
462
|
-
case "
|
|
463
|
-
if (e.kind !== "
|
|
464
|
-
if (e.stage === "press") return false;
|
|
465
|
-
if (!matchModifiers(e, spec.mods, isMac)) return false;
|
|
466
|
-
return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
|
|
467
|
-
}
|
|
468
|
-
case "pointerDown": {
|
|
469
|
-
if (e.kind !== "pointerdown" || e.stage !== "press") return false;
|
|
473
|
+
case "hover": {
|
|
474
|
+
if (e.kind !== "hover") return false;
|
|
470
475
|
if (!matchModifiers(e, spec.mods, isMac)) return false;
|
|
471
|
-
|
|
476
|
+
const on = !e.off && matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
|
|
477
|
+
if (spec.motion !== "leave") return on;
|
|
478
|
+
return !on && e.from !== void 0 && matchTarget(e.from.affordance, spec.target, e.from.bodyTarget, e.from.bodyKind);
|
|
472
479
|
}
|
|
480
|
+
case "drag":
|
|
481
|
+
case "pointerDown":
|
|
482
|
+
return matchPress(e, spec, isMac);
|
|
473
483
|
case "multiTouch": {
|
|
474
484
|
if (e.kind !== "multitouch") return false;
|
|
475
485
|
if (e.fingers !== spec.fingers) return false;
|
|
@@ -557,9 +567,10 @@ function routeToSpec(parsed) {
|
|
|
557
567
|
switch (kind) {
|
|
558
568
|
case void 0:
|
|
559
569
|
return fail(`${parsed.gesture} has no GestureSpec kind`);
|
|
570
|
+
case "hover":
|
|
571
|
+
return withMods({ kind, ...arg === "leave" ? { motion: "leave" } : {}, ...t });
|
|
560
572
|
case "click":
|
|
561
573
|
case "pointerDown":
|
|
562
|
-
case "hover":
|
|
563
574
|
case "doubleClick":
|
|
564
575
|
case "drag":
|
|
565
576
|
case "contextMenu":
|
|
@@ -626,6 +637,10 @@ function partitionModifiers(mods2) {
|
|
|
626
637
|
}
|
|
627
638
|
return { required, optional };
|
|
628
639
|
}
|
|
640
|
+
function leftClause(target) {
|
|
641
|
+
if (target === void 0 || target === "*") return "off the surface";
|
|
642
|
+
return target === "empty" ? "off empty canvas" : `off a ${target}`;
|
|
643
|
+
}
|
|
629
644
|
function phaseAtomParts({ channel, phase }) {
|
|
630
645
|
const subject = channel === "&" ? "the tool" : channel === "*" ? "any tool" : `the ${channel} tool`;
|
|
631
646
|
if (phase === "initial") return [`${subject} is `, term("idle")];
|
|
@@ -688,7 +703,7 @@ function actionClause(parsed, required) {
|
|
|
688
703
|
case "pointerDown":
|
|
689
704
|
return userDoes(required, `presses${target}`);
|
|
690
705
|
case "hover":
|
|
691
|
-
return userDoes(required, `moves the pointer${target}`);
|
|
706
|
+
return userDoes(required, arg === "leave" ? `moves the pointer ${leftClause(parsed.target)}` : `moves the pointer${target}`);
|
|
692
707
|
case "dblTap":
|
|
693
708
|
return userDoes(required, `double-taps${target}`);
|
|
694
709
|
case "drag":
|
|
@@ -1172,6 +1187,65 @@ function isInControlWithin(target, host) {
|
|
|
1172
1187
|
return control !== null && control !== host && host.contains(control);
|
|
1173
1188
|
}
|
|
1174
1189
|
|
|
1190
|
+
// src/ui/heldPointers.ts
|
|
1191
|
+
var trackers = /* @__PURE__ */ new WeakMap();
|
|
1192
|
+
function attach(element) {
|
|
1193
|
+
const doc = element.ownerDocument;
|
|
1194
|
+
const win = doc.defaultView;
|
|
1195
|
+
const held = /* @__PURE__ */ new Map();
|
|
1196
|
+
const down = (e) => {
|
|
1197
|
+
const p = e;
|
|
1198
|
+
held.set(p.pointerId, !!p.buttons);
|
|
1199
|
+
};
|
|
1200
|
+
const up = (e) => {
|
|
1201
|
+
held.delete(e.pointerId);
|
|
1202
|
+
};
|
|
1203
|
+
const move = (e) => {
|
|
1204
|
+
const p = e;
|
|
1205
|
+
if (held.get(p.pointerId) && p.buttons === 0) held.delete(p.pointerId);
|
|
1206
|
+
};
|
|
1207
|
+
const blur = () => {
|
|
1208
|
+
held.clear();
|
|
1209
|
+
};
|
|
1210
|
+
element.addEventListener("pointerdown", down, true);
|
|
1211
|
+
doc.addEventListener("pointerup", up, true);
|
|
1212
|
+
doc.addEventListener("pointercancel", up, true);
|
|
1213
|
+
doc.addEventListener("pointermove", move, true);
|
|
1214
|
+
win?.addEventListener("blur", blur);
|
|
1215
|
+
return {
|
|
1216
|
+
readers: 0,
|
|
1217
|
+
held,
|
|
1218
|
+
detach() {
|
|
1219
|
+
element.removeEventListener("pointerdown", down, true);
|
|
1220
|
+
doc.removeEventListener("pointerup", up, true);
|
|
1221
|
+
doc.removeEventListener("pointercancel", up, true);
|
|
1222
|
+
doc.removeEventListener("pointermove", move, true);
|
|
1223
|
+
win?.removeEventListener("blur", blur);
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function heldPointers(element) {
|
|
1228
|
+
let tracker = trackers.get(element);
|
|
1229
|
+
if (!tracker) {
|
|
1230
|
+
tracker = attach(element);
|
|
1231
|
+
trackers.set(element, tracker);
|
|
1232
|
+
}
|
|
1233
|
+
const shared = tracker;
|
|
1234
|
+
shared.readers += 1;
|
|
1235
|
+
let released = false;
|
|
1236
|
+
return {
|
|
1237
|
+
count: () => shared.held.size,
|
|
1238
|
+
release() {
|
|
1239
|
+
if (released) return;
|
|
1240
|
+
released = true;
|
|
1241
|
+
shared.readers -= 1;
|
|
1242
|
+
if (shared.readers > 0) return;
|
|
1243
|
+
shared.detach();
|
|
1244
|
+
if (trackers.get(element) === shared) trackers.delete(element);
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1175
1249
|
// src/ui/keyOwner.ts
|
|
1176
1250
|
function isEditableTarget(target) {
|
|
1177
1251
|
if (!target || !(target instanceof Element)) return false;
|
|
@@ -1213,6 +1287,6 @@ function focusedControlOwnsKey(e) {
|
|
|
1213
1287
|
return activatesFocusedControl(e) || selectOwnsKey(e);
|
|
1214
1288
|
}
|
|
1215
1289
|
|
|
1216
|
-
export { ACTIVE_SIGILS, CONTROL_SELECTOR, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, LONG_PRESS_MS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, TAP_THRESHOLD_PX, UNKNOWN_MIME, VALID_MOD_NAMES, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
|
|
1290
|
+
export { ACTIVE_SIGILS, CONTROL_SELECTOR, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, LONG_PRESS_MS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, TAP_THRESHOLD_PX, UNKNOWN_MIME, VALID_MOD_NAMES, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
|
|
1217
1291
|
//# sourceMappingURL=index.js.map
|
|
1218
1292
|
//# sourceMappingURL=index.js.map
|