1
1
import clsx from "clsx"
2
- import { useAtomValue } from "jotai"
3
- import { FC , useCallback , useEffect , useRef , useState } from "react"
4
- import { ActiveTab , ActualDate , useMutateTab } from "../store"
2
+ import { useAtomValue , useSetAtom } from "jotai"
3
+ import { FC , useEffect , useRef , useState } from "react"
4
+ import { ActiveTab , TlSelected , useMutateTab } from "../store"
5
5
import { Place } from "../utils/geonames"
6
- import { useInteraction } from "../utils/useInteraction"
7
- import { useOnClickOutside } from "../utils/useOnClickOutside"
8
- import { BoardDefaultHead , BoardSelectHead , DateRangeISO } from "./BoardHead"
6
+ import { BoardHead } from "./BoardHead"
7
+ import { BoardLine } from "./BoardLine"
9
8
import { Timeline } from "./Timeline"
10
9
11
10
export const Board : FC = ( ) => {
12
- const { reorderPlaces } = useMutateTab ( )
13
11
const { places : rawPlaces } = useAtomValue ( ActiveTab )
14
12
const [ ordered , setOrdered ] = useState < Place [ ] > ( [ ] )
13
+ const { reorderPlaces } = useMutateTab ( )
14
+ const setTlSelected = useSetAtom ( TlSelected )
15
15
16
- const date = useAtomValue ( ActualDate )
17
-
18
- const [ range , setRange ] = useState ( { height : 0 , top : 0 , left : 0 , opacity : 0 } )
19
- const [ holdOn , setHoldOn ] = useState < string | null > ( null )
20
- const [ duration , setDuration ] = useState < DateRangeISO | null > ( null )
21
- const timelinesRef = useRef < HTMLDivElement > ( null )
22
-
23
- const calcLine = useCallback ( ( ) => {
24
- const cc = document . querySelector ( "[data-home=true] [data-current=true]" )
25
- if ( ! timelinesRef . current || ! cc ) {
26
- setRange ( ( old ) => ( { ...old , opacity : 0 } ) )
27
- return
28
- }
29
-
30
- const el = cc . getBoundingClientRect ( )
31
- setLineView ( el . left , el . width )
32
- } , [ ] )
33
-
34
- const setLineView = ( x : number , width : number ) => {
35
- if ( ! timelinesRef . current ) return
36
- const rt = timelinesRef . current . getBoundingClientRect ( )
37
-
38
- setRange ( ( old ) => ( {
39
- ...old ,
40
- opacity : 1 ,
41
- top : rt . height / 2 ,
42
- height : rt . height - 16 ,
43
- left : x - rt . left ,
44
- width : width ,
45
- // background: "rgba(255, 255, 255, 0.05)",
46
- } ) )
47
- }
48
-
49
- useEffect ( ( ) => {
50
- setOrdered ( [ ...rawPlaces ] )
51
-
52
- Array . from ( document . querySelectorAll ( ".animate-tick" ) )
53
- . flatMap ( ( x ) => x . getAnimations ( ) )
54
- . forEach ( ( x ) => {
55
- x . cancel ( )
56
- x . play ( )
57
- } )
58
- } , [ rawPlaces ] )
16
+ const rtRef = useRef < HTMLDivElement > ( null )
17
+ const tlRef = useRef < HTMLDivElement > ( null )
18
+ const tlIdxRef = useRef < string | null > ( null )
19
+ const orderedRef = useRef < Place [ ] > ( ordered )
20
+ orderedRef . current = ordered
59
21
60
22
useEffect ( ( ) => {
61
- const timeoutId = setTimeout ( ( ) => {
62
- setHoldOn ( null )
63
- setDuration ( null )
64
- calcLine ( )
65
- } , 1 )
66
- return ( ) => clearTimeout ( timeoutId )
67
- } , [ date , rawPlaces ] )
68
-
69
- useInteraction ( timelinesRef , {
70
- start ( e ) {
71
- setHoldOn ( null )
72
-
73
- const dn = ( e . target as HTMLElement ) ?. closest ( "[data-drag-node]" ) as HTMLElement
74
- if ( dn ) {
75
- const dr = dn . closest ( "[data-drag-root]" ) ! as HTMLElement
76
- dr . setAttribute ( "data-dragging" , "true" )
77
- dr . style . visibility = "hidden"
78
-
79
- const cp = dr ! . cloneNode ( true ) as HTMLElement
80
- cp . setAttribute ( "data-dragging-clone" , "true" )
81
- cp . style . pointerEvents = "none"
82
- cp . style . visibility = "visible"
83
- cp . style . position = "fixed"
84
-
85
- const cr = dr . getBoundingClientRect ( )
86
- cp . style . top = `${ cr . top } px`
87
- cp . style . left = `${ cr . left } px`
88
- cp . style . width = `${ cr . width } px`
89
- cp . style . height = `${ cr . height } px`
90
- document . body . appendChild ( cp )
91
- return
23
+ type Event = MouseEvent | TouchEvent
24
+
25
+ const onStart = ( e : Event ) => {
26
+ if ( ! tlRef . current ) return
27
+ tlIdxRef . current = null
28
+
29
+ const node = ( e . target as HTMLElement ) ?. closest ( "[data-drag-node]" ) as HTMLElement
30
+ if ( node ) {
31
+ setTlSelected ( null )
32
+
33
+ const root = node . closest ( "[data-drag-root]" ) ! as HTMLElement
34
+ root . setAttribute ( "data-dragging" , "true" )
35
+ root . style . visibility = "hidden"
36
+
37
+ const fake = root ! . cloneNode ( true ) as HTMLElement
38
+ fake . setAttribute ( "data-dragging-clone" , "true" )
39
+ fake . style . pointerEvents = "none"
40
+ fake . style . visibility = "visible"
41
+ fake . style . position = "fixed"
42
+
43
+ const rect = root . getBoundingClientRect ( )
44
+ fake . style . top = `${ rect . top } px`
45
+ fake . style . left = `${ rect . left } px`
46
+ fake . style . width = `${ rect . width } px`
47
+ fake . style . height = `${ rect . height } px`
48
+ document . body . appendChild ( fake )
49
+ } else {
50
+ const node = ( e . target as HTMLElement ) ?. closest ( "[data-tl-idx]" )
51
+ if ( ! node ) return
52
+
53
+ tlIdxRef . current = node . getAttribute ( "data-tl-idx" )
54
+ const idx = node . getAttribute ( "data-tl-idx" ) !
55
+ setTlSelected ( [ idx , idx ] )
92
56
}
57
+ }
93
58
94
- const ce = ( e . target as HTMLElement ) ?. closest ( "[data-datetime]" )
95
- if ( ! ce || ! timelinesRef . current ) return
96
-
97
- const el = ce . getBoundingClientRect ( )
98
- setLineView ( el . left , el . width )
99
- setHoldOn ( ce . getAttribute ( "data-datetime" ) )
100
-
101
- const dd = ce . getAttribute ( "data-datetime" ) ! . split ( "~" ) [ 0 ]
102
- setDuration ( [ dd , dd ] )
103
- } ,
59
+ const onMove = ( e : Event ) => {
60
+ if ( ! tlRef . current ) return
104
61
105
- move ( e ) {
106
- // const ex = "clientX" in e ? e.clientX : e.touches[0].clientX
107
62
const ey = "clientY" in e ? e . clientY : e . touches [ 0 ] . clientY
108
63
109
- // update drag clone – NO RETURN
110
- const cp = document . querySelector ( "[data-dragging-clone]" ) as HTMLElement
111
- if ( cp && timelinesRef . current ) {
112
- const { top, bottom } = timelinesRef . current . getBoundingClientRect ( )
113
- const ny = Math . min ( bottom - cp . getBoundingClientRect ( ) . height , Math . max ( top , ey ) )
114
- cp . style . top = `${ ny } px`
64
+ // update drag clone
65
+ const fake = document . querySelector ( "[data-dragging-clone]" ) as HTMLElement
66
+ if ( fake ) {
67
+ const { top, bottom } = tlRef . current . getBoundingClientRect ( )
68
+ const yy = Math . min ( bottom - fake . getBoundingClientRect ( ) . height , Math . max ( top , ey ) )
69
+ fake . style . top = `${ yy } px`
70
+ // NO RETURN HERE
115
71
}
116
72
117
73
// reorder places on drag
118
- const cc = document . querySelector ( "[data-dragging]" ) as HTMLElement
119
- if ( cc ) {
74
+ const drag = document . querySelector ( "[data-dragging]" ) as HTMLElement
75
+ if ( drag ) {
120
76
e . preventDefault ( )
121
77
122
78
const x = "clientX" in e ? e . clientX : e . touches [ 0 ] . clientX
123
79
const y = "clientY" in e ? e . clientY : e . touches [ 0 ] . clientY
124
80
125
- const all = Array . from ( timelinesRef . current ?. querySelectorAll ( "[data-drag-root]" ) ?? [ ] )
81
+ const all = Array . from ( tlRef . current ?. querySelectorAll ( "[data-drag-root]" ) ?? [ ] )
126
82
const els = document . elementsFromPoint ( x , y ) . filter ( ( x ) => x . hasAttribute ( "data-drag-root" ) )
127
- const wasIdx = all . indexOf ( cc )
83
+ const wasIdx = all . indexOf ( drag )
128
84
const nowIdx = all . indexOf ( els [ 0 ] )
129
85
if ( wasIdx === nowIdx || wasIdx === - 1 || nowIdx === - 1 ) return
130
86
@@ -136,71 +92,72 @@ export const Board: FC = () => {
136
92
return
137
93
}
138
94
139
- // update selection
140
- if ( holdOn ) {
141
- const nowEl = ( e . target as HTMLElement ) ?. closest ( "[data-datetime]" )
142
- const wasEl = document . querySelector ( `[data-datetime="${ holdOn } "]` )
143
- if ( ! timelinesRef . current || ! nowEl || ! wasEl ) return
144
-
145
- const dd = [ nowEl , wasEl ]
146
- . map ( ( x ) => x . getAttribute ( "data-datetime" ) ! . split ( "~" ) [ 0 ] )
147
- . sort ( ) as DateRangeISO
148
- setDuration ( dd )
95
+ // update timeline selection
96
+ if ( tlIdxRef . current ) {
97
+ const nowEl = ( e . target as HTMLElement ) ?. closest ( "[data-tl-idx]" ) as HTMLElement
98
+ const wasEl = document . querySelector ( `[data-tl-idx="${ tlIdxRef . current } "]` ) as HTMLElement
99
+ if ( ! nowEl || ! wasEl ) return
149
100
150
- const a = nowEl . getBoundingClientRect ( )
151
- const b = wasEl . getBoundingClientRect ( )
152
-
153
- const l = Math . min ( b . left , a . left )
154
- const w = b . left < a . left ? a . right - b . left : b . right - a . left
155
- setLineView ( l , w )
101
+ setTlSelected ( [ wasEl . getAttribute ( "data-tl-idx" ) ! , nowEl . getAttribute ( "data-tl-idx" ) ! ] )
156
102
}
157
- } ,
103
+ }
104
+
105
+ const onEnd = ( e : Event ) => {
106
+ tlIdxRef . current = null
158
107
159
- end ( e ) {
160
- const cc = document . querySelector ( "[data-dragging]" ) as HTMLElement
161
- if ( cc ) {
108
+ const drag = document . querySelector ( "[data-dragging]" ) as HTMLElement
109
+ if ( drag ) {
162
110
e . preventDefault ( )
163
111
document . querySelectorAll ( "[data-dragging-clone]" ) . forEach ( ( x ) => x . remove ( ) )
164
- cc . removeAttribute ( "data-dragging" )
165
- cc . style . visibility = "visible"
166
-
167
- reorderPlaces ( ordered . map ( ( x ) => x . id ) )
168
- return
112
+ drag . removeAttribute ( "data-dragging" )
113
+ drag . style . visibility = "visible"
114
+ reorderPlaces ( orderedRef . current . map ( ( x ) => x . id ) )
169
115
}
116
+ }
170
117
171
- setHoldOn ( null )
172
- } ,
173
- } )
118
+ tlRef . current ?. addEventListener ( "mousedown" , onStart )
119
+ tlRef . current ?. addEventListener ( "touchstart" , onStart )
120
+ document . addEventListener ( "mousemove" , onMove )
121
+ document . addEventListener ( "touchmove" , onMove )
122
+ document . addEventListener ( "mouseup" , onEnd )
123
+ document . addEventListener ( "touchend" , onEnd )
124
+
125
+ return ( ) => {
126
+ tlRef . current ?. removeEventListener ( "mousedown" , onStart )
127
+ tlRef . current ?. removeEventListener ( "touchstart" , onStart )
128
+ document . removeEventListener ( "mousemove" , onMove )
129
+ document . removeEventListener ( "touchmove" , onMove )
130
+ document . removeEventListener ( "mouseup" , onEnd )
131
+ document . removeEventListener ( "touchend" , onEnd )
132
+ }
133
+ } , [ ] )
174
134
175
- const rootRef = useRef < HTMLDivElement > ( null )
176
- useOnClickOutside ( rootRef , ( ) => {
177
- setHoldOn ( null )
178
- setDuration ( null )
179
- calcLine ( )
180
- } )
135
+ useEffect ( ( ) => {
136
+ setOrdered ( [ ...rawPlaces ] )
137
+
138
+ Array . from ( document . querySelectorAll ( ".animate-tick" ) )
139
+ . flatMap ( ( x ) => x . getAnimations ( ) )
140
+ . forEach ( ( x ) => {
141
+ x . cancel ( )
142
+ x . play ( )
143
+ } )
144
+ } , [ rawPlaces ] )
181
145
182
146
return (
183
- < div className = "flex flex-col" ref = { rootRef } >
147
+ < div ref = { rtRef } className = "flex flex-col" >
184
148
< div className = "h-[48px] w-full" >
185
- { duration ? < BoardSelectHead duration = { duration } /> : < BoardDefaultHead /> }
149
+ < BoardHead />
186
150
</ div >
187
151
188
152
< div
189
- ref = { timelinesRef }
153
+ ref = { tlRef }
190
154
onWheel = { ( e ) => ( e . currentTarget . scrollLeft += e . deltaY > 0 ? 75 : - 75 ) }
191
155
className = { clsx (
192
156
"relative box-border flex flex-col border-t py-2" ,
193
157
"no-scrollbar overflow-x-scroll" ,
194
158
) }
195
159
>
196
- < div
197
- style = { range }
198
- className = { clsx (
199
- "pointer-events-none absolute z-[12] w-[32px] select-none rounded-md" ,
200
- "border-2 border-red-500/50 dark:border-red-500/80" ,
201
- "box-border -translate-y-1/2" ,
202
- ) }
203
- > </ div >
160
+ < BoardLine rtRef = { rtRef } tlRef = { tlRef } />
204
161
205
162
< div key = "timelines" >
206
163
{ ordered . map ( ( x ) => (
0 commit comments