@lotics/ui 1.11.1 → 1.12.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/package.json +1 -1
- package/src/calendar/calendar_view.tsx +5 -1
- package/src/calendar/month_view.tsx +40 -4
- package/src/css_modules.d.ts +2 -0
- package/src/gantt/gantt_view.tsx +31 -5
- package/src/popover.tsx +28 -2
- package/src/use_pointer_drag.ts +99 -0
package/package.json
CHANGED
|
@@ -20,6 +20,9 @@ export interface CalendarViewProps<T = unknown> {
|
|
|
20
20
|
labels?: Partial<CalendarLabels>;
|
|
21
21
|
onEventPress?: (event: CalendarEvent<T>) => void;
|
|
22
22
|
onDayPress?: (day: Date) => void;
|
|
23
|
+
/** When set, month-view event chips become draggable; dropping on another day
|
|
24
|
+
* reschedules the event (duration preserved). Receives event + new start/end. */
|
|
25
|
+
onEventDrop?: (event: CalendarEvent<T>, newStart: Date, newEnd: Date | null) => void;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -28,7 +31,7 @@ export interface CalendarViewProps<T = unknown> {
|
|
|
28
31
|
* primitive is drop-in; the consumer only supplies events + callbacks.
|
|
29
32
|
*/
|
|
30
33
|
export function CalendarView<T = unknown>(props: CalendarViewProps<T>) {
|
|
31
|
-
const { events, defaultView = "month", defaultDate, weekStartsOn = 1, locale, onEventPress, onDayPress } = props;
|
|
34
|
+
const { events, defaultView = "month", defaultDate, weekStartsOn = 1, locale, onEventPress, onDayPress, onEventDrop } = props;
|
|
32
35
|
const L = { ...DEFAULT_CALENDAR_LABELS, ...props.labels };
|
|
33
36
|
const [view, setView] = useState<CalendarViewMode>(defaultView);
|
|
34
37
|
const [date, setDate] = useState<Date>(defaultDate ?? new Date());
|
|
@@ -88,6 +91,7 @@ export function CalendarView<T = unknown>(props: CalendarViewProps<T>) {
|
|
|
88
91
|
moreLabel={L.more}
|
|
89
92
|
onEventPress={onEventPress}
|
|
90
93
|
onDayPress={drillToDay}
|
|
94
|
+
onEventDrop={onEventDrop}
|
|
91
95
|
/>
|
|
92
96
|
) : (
|
|
93
97
|
<TimeGridView
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useMemo, useRef } from "react";
|
|
2
2
|
import { View, Pressable, StyleSheet } from "react-native";
|
|
3
3
|
import { Text } from "../text";
|
|
4
4
|
import { colors } from "../colors";
|
|
5
|
+
import { usePointerDrag } from "../use_pointer_drag";
|
|
5
6
|
import { packEventLanes } from "./layout";
|
|
6
|
-
import { daysInView, isSameMonth, isToday, weekdayShort } from "./dates";
|
|
7
|
+
import { addDays, dayDiff, daysInView, isSameMonth, isToday, startOfDay, weekdayShort } from "./dates";
|
|
7
8
|
import type { CalendarEvent, Weekday } from "./types";
|
|
8
9
|
|
|
9
10
|
const LANE_H = 19;
|
|
@@ -19,15 +20,39 @@ export interface MonthViewProps<T = unknown> {
|
|
|
19
20
|
moreLabel?: (count: number) => string;
|
|
20
21
|
onEventPress?: (event: CalendarEvent<T>) => void;
|
|
21
22
|
onDayPress?: (day: Date) => void;
|
|
23
|
+
/** When set, event chips become draggable; dropping on another day reschedules
|
|
24
|
+
* the event, preserving its duration. Receives the event + new start/end. */
|
|
25
|
+
onEventDrop?: (event: CalendarEvent<T>, newStart: Date, newEnd: Date | null) => void;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
25
|
-
const { date, events, weekStartsOn = 1, locale, moreLabel = (n) => `+${n} more`, onEventPress, onDayPress } = props;
|
|
29
|
+
const { date, events, weekStartsOn = 1, locale, moreLabel = (n) => `+${n} more`, onEventPress, onDayPress, onEventDrop } = props;
|
|
26
30
|
const days = useMemo(() => daysInView("month", date, weekStartsOn), [date, weekStartsOn]);
|
|
27
31
|
const weeks = useMemo(() => Array.from({ length: 6 }, (_, w) => days.slice(w * 7, w * 7 + 7)), [days]);
|
|
28
32
|
const weekdayLabels = useMemo(() => days.slice(0, 7).map((d) => weekdayShort(d, locale)), [days, locale]);
|
|
29
33
|
const now = new Date();
|
|
30
34
|
|
|
35
|
+
// Drag-to-reschedule: each week row registers its DOM rect; on drop we map the
|
|
36
|
+
// pointer to a (week, column) → day, then shift the event by whole days.
|
|
37
|
+
const eventById = useMemo(() => new Map(events.map((e) => [e.id, e])), [events]);
|
|
38
|
+
const weekNodes = useRef<Map<number, HTMLElement>>(new Map());
|
|
39
|
+
const { live, bind } = usePointerDrag((id, pointer) => {
|
|
40
|
+
const ev = eventById.get(id);
|
|
41
|
+
if (!ev || !onEventDrop) return;
|
|
42
|
+
for (const [w, el] of weekNodes.current) {
|
|
43
|
+
const r = el.getBoundingClientRect();
|
|
44
|
+
if (r.width > 0 && pointer.y >= r.top && pointer.y <= r.bottom) {
|
|
45
|
+
const col = Math.min(6, Math.max(0, Math.floor((pointer.x - r.left) / (r.width / 7))));
|
|
46
|
+
const target = days[w * 7 + col];
|
|
47
|
+
if (!target) return;
|
|
48
|
+
const shift = dayDiff(startOfDay(ev.start), target);
|
|
49
|
+
if (shift === 0) return;
|
|
50
|
+
onEventDrop(ev, addDays(ev.start, shift), ev.end ? addDays(ev.end, shift) : null);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
31
56
|
return (
|
|
32
57
|
<View style={styles.root}>
|
|
33
58
|
<View style={styles.weekdayHeader}>
|
|
@@ -49,7 +74,15 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
return (
|
|
52
|
-
<View
|
|
77
|
+
<View
|
|
78
|
+
key={w}
|
|
79
|
+
style={styles.weekRow}
|
|
80
|
+
ref={(node) => {
|
|
81
|
+
const el = node as unknown as HTMLElement | null;
|
|
82
|
+
if (el) weekNodes.current.set(w, el);
|
|
83
|
+
else weekNodes.current.delete(w);
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
53
86
|
{/* Date numbers */}
|
|
54
87
|
<View style={styles.dateRow}>
|
|
55
88
|
{weekDays.map((day) => {
|
|
@@ -89,9 +122,11 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
89
122
|
{visible.map((bar) => {
|
|
90
123
|
const accent = bar.event.color || colors.teal[600];
|
|
91
124
|
const banner = bar.span > 1 || !!bar.event.allDay;
|
|
125
|
+
const drag = live && live.id === bar.event.id ? live : null;
|
|
92
126
|
return (
|
|
93
127
|
<Pressable
|
|
94
128
|
key={bar.event.id}
|
|
129
|
+
ref={onEventDrop ? bind(bar.event.id, "grab") : undefined}
|
|
95
130
|
onPress={onEventPress ? () => onEventPress(bar.event) : undefined}
|
|
96
131
|
accessibilityRole={onEventPress ? "button" : undefined}
|
|
97
132
|
accessibilityLabel={bar.event.title}
|
|
@@ -102,6 +137,7 @@ export function MonthView<T = unknown>(props: MonthViewProps<T>) {
|
|
|
102
137
|
left: `${(bar.startCol / 7) * 100}%`,
|
|
103
138
|
width: `${(bar.span / 7) * 100}%`,
|
|
104
139
|
paddingHorizontal: 2,
|
|
140
|
+
...(drag ? { transform: [{ translateX: drag.dx }, { translateY: drag.dy }], zIndex: 20, opacity: 0.9 } : null),
|
|
105
141
|
}}
|
|
106
142
|
>
|
|
107
143
|
<View
|
package/src/css_modules.d.ts
CHANGED
package/src/gantt/gantt_view.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { View, ScrollView, Pressable, StyleSheet } from "react-native";
|
|
3
3
|
import { Text } from "../text";
|
|
4
4
|
import { colors } from "../colors";
|
|
5
|
-
import { dayDiff } from "../calendar/dates";
|
|
5
|
+
import { addDays, dayDiff } from "../calendar/dates";
|
|
6
|
+
import { usePointerDrag } from "../use_pointer_drag";
|
|
6
7
|
import { axisRange, barGeometry, buildTicks, pxPerDay } from "./scale";
|
|
7
8
|
import { DEFAULT_GANTT_LABELS } from "./types";
|
|
8
9
|
import type { GanttLabels, GanttScale, GanttTask } from "./types";
|
|
@@ -11,6 +12,8 @@ const LABEL_W = 188;
|
|
|
11
12
|
const HEADER_H = 38;
|
|
12
13
|
const ROW_H = 40;
|
|
13
14
|
const BAR_H = 22;
|
|
15
|
+
const HANDLE_W = 8;
|
|
16
|
+
const MIN_BAR = 8;
|
|
14
17
|
const SCALES: GanttScale[] = ["day", "week", "month"];
|
|
15
18
|
|
|
16
19
|
export interface GanttViewProps<T = unknown> {
|
|
@@ -23,6 +26,9 @@ export interface GanttViewProps<T = unknown> {
|
|
|
23
26
|
/** User-facing chrome strings; defaults to English. */
|
|
24
27
|
labels?: Partial<GanttLabels>;
|
|
25
28
|
onTaskPress?: (task: GanttTask<T>) => void;
|
|
29
|
+
/** When set, each bar gets a right-edge resize handle; dragging it adjusts the
|
|
30
|
+
* task's end. Receives the task + the new inclusive end day (clamped ≥ start). */
|
|
31
|
+
onTaskResize?: (task: GanttTask<T>, newEnd: Date) => void;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -33,10 +39,20 @@ export interface GanttViewProps<T = unknown> {
|
|
|
33
39
|
* — wrap in a ScrollView for very long task lists.
|
|
34
40
|
*/
|
|
35
41
|
export function GanttView<T = unknown>(props: GanttViewProps<T>) {
|
|
36
|
-
const { tasks, defaultScale = "week", today = new Date(), locale, title, onTaskPress } = props;
|
|
42
|
+
const { tasks, defaultScale = "week", today = new Date(), locale, title, onTaskPress, onTaskResize } = props;
|
|
37
43
|
const L = { ...DEFAULT_GANTT_LABELS, ...props.labels };
|
|
38
44
|
const [scale, setScale] = useState<GanttScale>(defaultScale);
|
|
39
45
|
|
|
46
|
+
const taskById = useMemo(() => new Map(tasks.map((t) => [t.id, t])), [tasks]);
|
|
47
|
+
const { live, bind } = usePointerDrag((id, _pointer, delta) => {
|
|
48
|
+
const t = taskById.get(id);
|
|
49
|
+
if (!t || !onTaskResize) return;
|
|
50
|
+
const days = Math.round(delta.dx / pxPerDay(scale));
|
|
51
|
+
if (days === 0) return;
|
|
52
|
+
const newEnd = addDays(t.end ?? t.start, days);
|
|
53
|
+
onTaskResize(t, newEnd < t.start ? t.start : newEnd);
|
|
54
|
+
});
|
|
55
|
+
|
|
40
56
|
const { start: axisStart, end: axisEnd } = useMemo(() => axisRange(tasks, today), [tasks, today]);
|
|
41
57
|
const ticks = useMemo(() => buildTicks(axisStart, axisEnd, scale, locale), [axisStart, axisEnd, scale, locale]);
|
|
42
58
|
const axisWidth = useMemo(() => (dayDiff(axisStart, axisEnd) + 1) * pxPerDay(scale), [axisStart, axisEnd, scale]);
|
|
@@ -100,7 +116,9 @@ export function GanttView<T = unknown>(props: GanttViewProps<T>) {
|
|
|
100
116
|
|
|
101
117
|
{/* Task rows */}
|
|
102
118
|
{tasks.map((t) => {
|
|
103
|
-
const
|
|
119
|
+
const base = barGeometry(t, axisStart, scale);
|
|
120
|
+
const dragging = live && live.id === t.id ? live : null;
|
|
121
|
+
const width = dragging ? Math.max(MIN_BAR, base.width + dragging.dx) : base.width;
|
|
104
122
|
const accent = t.color || colors.teal[600];
|
|
105
123
|
return (
|
|
106
124
|
<View key={t.id} style={{ height: ROW_H, borderBottomWidth: 1, borderBottomColor: colors.zinc[100] }}>
|
|
@@ -113,12 +131,20 @@ export function GanttView<T = unknown>(props: GanttViewProps<T>) {
|
|
|
113
131
|
onPress={onTaskPress ? () => onTaskPress(t) : undefined}
|
|
114
132
|
accessibilityRole={onTaskPress ? "button" : undefined}
|
|
115
133
|
accessibilityLabel={t.label}
|
|
116
|
-
style={{ position: "absolute", left:
|
|
134
|
+
style={{ position: "absolute", left: base.left, width, top: (ROW_H - BAR_H) / 2, height: BAR_H }}
|
|
117
135
|
>
|
|
118
|
-
<View style={{ flex: 1, borderRadius: 5, backgroundColor: accent, justifyContent: "center", paddingHorizontal: 8 }}>
|
|
136
|
+
<View style={{ flex: 1, borderRadius: 5, backgroundColor: accent, justifyContent: "center", paddingHorizontal: 8, opacity: dragging ? 0.85 : 1 }}>
|
|
119
137
|
<Text size="xs" weight="medium" numberOfLines={1} style={{ color: colors.white }}>{t.label}</Text>
|
|
120
138
|
</View>
|
|
121
139
|
</Pressable>
|
|
140
|
+
{onTaskResize ? (
|
|
141
|
+
<View
|
|
142
|
+
ref={bind(t.id, "ew-resize")}
|
|
143
|
+
style={{ position: "absolute", left: base.left + width - HANDLE_W, width: HANDLE_W + 6, top: (ROW_H - BAR_H) / 2, height: BAR_H, alignItems: "center", justifyContent: "center", zIndex: 2 }}
|
|
144
|
+
>
|
|
145
|
+
<View style={{ width: 3, height: 11, borderRadius: 2, backgroundColor: "rgba(255,255,255,0.75)" }} />
|
|
146
|
+
</View>
|
|
147
|
+
) : null}
|
|
122
148
|
</View>
|
|
123
149
|
);
|
|
124
150
|
})}
|
package/src/popover.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import React, {
|
|
|
3
3
|
useCallback,
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
8
9
|
} from "react";
|
|
@@ -203,12 +204,29 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
203
204
|
const [triggerWidth, setTriggerWidth] = useState<number>(0);
|
|
204
205
|
const [isBottomSheetShown, setIsBottomSheetShown] = useState(false);
|
|
205
206
|
const returnFocusRef = useRef<HTMLElement | null>(null);
|
|
207
|
+
// Last known trigger geometry. The trigger can unmount while the popover is
|
|
208
|
+
// open — a hover-revealed menu button stops being rendered the moment our
|
|
209
|
+
// overlay covers its row and the hover ends — so positioning cannot rely on
|
|
210
|
+
// `triggerRef.current` still being live when it runs.
|
|
211
|
+
const triggerRectRef = useRef<DOMRect | null>(null);
|
|
206
212
|
|
|
207
213
|
const handleClose = useCallback(() => {
|
|
208
214
|
if (!open) return;
|
|
209
215
|
onOpenChange(false);
|
|
210
216
|
}, [onOpenChange, open]);
|
|
211
217
|
|
|
218
|
+
// Snapshot the trigger geometry synchronously the moment we open, while it is
|
|
219
|
+
// guaranteed to still be mounted. `calculatePosition` runs later in a rAF, by
|
|
220
|
+
// which point a hover-revealed trigger may already be gone (our overlay covers
|
|
221
|
+
// its row, hover ends, the button unmounts). Capturing here keeps the popover
|
|
222
|
+
// anchored to where the trigger was instead of stranding it off-screen.
|
|
223
|
+
useLayoutEffect(() => {
|
|
224
|
+
if (!open || small) return;
|
|
225
|
+
if (triggerRef.current) {
|
|
226
|
+
triggerRectRef.current = triggerRef.current.getBoundingClientRect();
|
|
227
|
+
}
|
|
228
|
+
}, [open, small, triggerRef]);
|
|
229
|
+
|
|
212
230
|
// Focus management: when the popover opens, remember what had focus and move
|
|
213
231
|
// focus into the popover (the content div is tab-able via `tabIndex=-1`).
|
|
214
232
|
// When it closes, restore focus to the prior element so keyboard users don't
|
|
@@ -280,10 +298,17 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
280
298
|
}, [open, small, onOpenChange]);
|
|
281
299
|
|
|
282
300
|
const calculatePosition = useCallback(() => {
|
|
283
|
-
if (!
|
|
301
|
+
if (!popoverRef.current) return;
|
|
284
302
|
if (small) return;
|
|
285
303
|
|
|
286
|
-
|
|
304
|
+
// Prefer the live trigger when it is still mounted (so the popover tracks
|
|
305
|
+
// scroll/layout); fall back to the rect captured at open time when the
|
|
306
|
+
// trigger has since unmounted, instead of stranding the popover off-screen.
|
|
307
|
+
const liveTriggerRect = triggerRef.current?.getBoundingClientRect();
|
|
308
|
+
if (liveTriggerRect) triggerRectRef.current = liveTriggerRect;
|
|
309
|
+
const triggerRect = triggerRectRef.current;
|
|
310
|
+
if (!triggerRect) return;
|
|
311
|
+
|
|
287
312
|
const popoverRect = popoverRef.current.getBoundingClientRect();
|
|
288
313
|
const viewportWidth = window.innerWidth;
|
|
289
314
|
const viewportHeight = window.innerHeight;
|
|
@@ -403,6 +428,7 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
403
428
|
if (!open) {
|
|
404
429
|
setPosition((previous) => (previous === null ? previous : null));
|
|
405
430
|
setIsBottomSheetShown((previous) => (previous ? false : previous));
|
|
431
|
+
triggerRectRef.current = null;
|
|
406
432
|
return;
|
|
407
433
|
}
|
|
408
434
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
const DRAG_THRESHOLD = 4; // px the pointer must travel before a press becomes a drag
|
|
5
|
+
|
|
6
|
+
/** Live state of an in-progress drag — the pointer delta from where it started. */
|
|
7
|
+
export interface DragLive {
|
|
8
|
+
id: string;
|
|
9
|
+
dx: number;
|
|
10
|
+
dy: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PointerDragApi {
|
|
14
|
+
/** Non-null only while a drag is active (past the threshold). Consumers render
|
|
15
|
+
* live feedback (translate / resize) from `dx`/`dy`. */
|
|
16
|
+
live: DragLive | null;
|
|
17
|
+
/** Ref for a draggable element — attaches a pointerdown listener tagged with
|
|
18
|
+
* `id` and sets the web-only `cursor` + `touch-action: none` on the node (so
|
|
19
|
+
* touch-drag doesn't scroll). Stable per id+cursor, so it doesn't thrash
|
|
20
|
+
* listeners across renders. */
|
|
21
|
+
bind: (id: string, cursor?: string) => (node: unknown) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Self-contained pointer-event drag for time-axis primitives (calendar event
|
|
26
|
+
* chips, gantt bar handles). Mirrors the kanban's proven approach — pointerdown
|
|
27
|
+
* on the element, a movement threshold to distinguish taps, then window-level
|
|
28
|
+
* pointermove/up — but with no floating clone and no column/card coupling. On
|
|
29
|
+
* release, `onDrop` receives the final client pointer position plus the total
|
|
30
|
+
* delta; the consumer maps that to a date. Web-only (RN has no pointer events);
|
|
31
|
+
* on native it's an inert no-op so taps/press handlers still work.
|
|
32
|
+
*/
|
|
33
|
+
export function usePointerDrag(
|
|
34
|
+
onDrop: (id: string, pointer: { x: number; y: number }, delta: { dx: number; dy: number }) => void,
|
|
35
|
+
): PointerDragApi {
|
|
36
|
+
const [live, setLive] = useState<DragLive | null>(null);
|
|
37
|
+
const press = useRef<{ id: string; x: number; y: number; active: boolean } | null>(null);
|
|
38
|
+
const onDropRef = useRef(onDrop);
|
|
39
|
+
onDropRef.current = onDrop;
|
|
40
|
+
|
|
41
|
+
// Per-id DOM node + its pointerdown handler, so we can detach on unmount/rebind.
|
|
42
|
+
const bound = useRef<Map<string, { el: HTMLElement; handler: (e: PointerEvent) => void }>>(new Map());
|
|
43
|
+
const refCache = useRef<Map<string, (node: unknown) => void>>(new Map());
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (Platform.OS !== "web") return;
|
|
47
|
+
const move = (e: PointerEvent) => {
|
|
48
|
+
const p = press.current;
|
|
49
|
+
if (!p) return;
|
|
50
|
+
const dx = e.clientX - p.x;
|
|
51
|
+
const dy = e.clientY - p.y;
|
|
52
|
+
if (!p.active && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
53
|
+
p.active = true;
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
setLive({ id: p.id, dx, dy });
|
|
56
|
+
};
|
|
57
|
+
const up = (e: PointerEvent) => {
|
|
58
|
+
const p = press.current;
|
|
59
|
+
press.current = null;
|
|
60
|
+
setLive(null);
|
|
61
|
+
if (p?.active) {
|
|
62
|
+
onDropRef.current(p.id, { x: e.clientX, y: e.clientY }, { dx: e.clientX - p.x, dy: e.clientY - p.y });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener("pointermove", move);
|
|
66
|
+
window.addEventListener("pointerup", up);
|
|
67
|
+
return () => {
|
|
68
|
+
window.removeEventListener("pointermove", move);
|
|
69
|
+
window.removeEventListener("pointerup", up);
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const bind = useCallback((id: string, cursor = "grab") => {
|
|
74
|
+
const key = `${id}:${cursor}`;
|
|
75
|
+
const cached = refCache.current.get(key);
|
|
76
|
+
if (cached) return cached;
|
|
77
|
+
const cb = (node: unknown) => {
|
|
78
|
+
const el = node as HTMLElement | null;
|
|
79
|
+
const prev = bound.current.get(id);
|
|
80
|
+
if (prev && prev.el !== el) {
|
|
81
|
+
prev.el.removeEventListener("pointerdown", prev.handler);
|
|
82
|
+
bound.current.delete(id);
|
|
83
|
+
}
|
|
84
|
+
if (!el || Platform.OS !== "web") return;
|
|
85
|
+
if (bound.current.has(id)) return;
|
|
86
|
+
el.style.touchAction = "none";
|
|
87
|
+
el.style.cursor = cursor;
|
|
88
|
+
const handler = (e: PointerEvent) => {
|
|
89
|
+
press.current = { id, x: e.clientX, y: e.clientY, active: false };
|
|
90
|
+
};
|
|
91
|
+
el.addEventListener("pointerdown", handler);
|
|
92
|
+
bound.current.set(id, { el, handler });
|
|
93
|
+
};
|
|
94
|
+
refCache.current.set(key, cb);
|
|
95
|
+
return cb;
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
return { live, bind };
|
|
99
|
+
}
|