Repository navigation
Expand file tree
/
Copy pathDropHover.tsx
More file actions
162 lines (142 loc) · 7.25 KB
/
Copy pathDropHover.tsx
File metadata and controls
162 lines (142 loc) · 7.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import React, { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { css, cx } from '../../styled-system/css'
import { dropHoverRecipe } from '../../styled-system/recipes'
import { token } from '../../styled-system/tokens'
import DropThoughtZone from '../@types/DropThoughtZone'
import SimplePath from '../@types/SimplePath'
import ThoughtId from '../@types/ThoughtId'
import { LongPressState } from '../constants'
import testFlags from '../e2e/testFlags'
import usePinDropHover from '../hooks/usePinDropHover'
import attributeEquals from '../selectors/attributeEquals'
import calculateAutofocus from '../selectors/calculateAutofocus'
import dropHoverColor from '../selectors/dropHoverColor'
import getSortPreference from '../selectors/getSortPreference'
import getThoughtById from '../selectors/getThoughtById'
import prevSibling from '../selectors/prevSibling'
import rootedParentOf from '../selectors/rootedParentOf'
import appendToPath from '../util/appendToPath'
import { compareReasonable } from '../util/compareThought'
import debugLog from '../util/debugLog'
import equalPath from '../util/equalPath'
import head from '../util/head'
import isDivider from '../util/isDivider'
import nonNull from '../util/nonNull'
import parentOf from '../util/parentOf'
/** Renders a drop-hover element unconditionally. */
const DropHover = ({ simplePath }: { simplePath: SimplePath }) => {
const dropHoverColorValue = useSelector(state => dropHoverColor(state, simplePath.length))
const isTableCol1 = useSelector(state =>
attributeEquals(state, head(rootedParentOf(state, simplePath)), '=view', 'Table'),
)
const animateHover = useSelector(state => {
const parent = parentOf(simplePath)
const autofocus = calculateAutofocus(state)
return autofocus(simplePath) === 'dim' && autofocus(parent) === 'hide'
})
const thoughtId = head(simplePath)
// Records each drop hover that is actually shown, so that a hovered drop target that renders no drop hover can be told apart in the debug log from one that does.
useEffect(() => {
debugLog.log('dropHover', { zone: 'thought', thoughtId })
}, [thoughtId])
const insideDivider = useSelector(state => isDivider(getThoughtById(state, thoughtId)?.value))
return (
<span
className={cx(
dropHoverRecipe({ insideDivider }),
css({
/* only add a margin on the Thought drop hover since Subthought drop hover does not need to offset the bullet */
/* Equivalent to -1.2em @ Font Size 18, but scales across Font Sizes 13–24. */
marginLeft: isTableCol1 ? 'calc(0.9em - 0.55em)' : 'calc(-0.4em - 0.55em)',
marginTop: '-0.1em',
_mobile: {
/* Equivalent to -1.2em @ Font Size 18, but scales across Font Sizes 13–24. */
marginLeft: 'calc(-0.4em - 14px)',
},
display: 'inline',
animation: animateHover ? `pulseLight {durations.mediumPulse} linear infinite alternate` : undefined,
}),
)}
style={{
backgroundColor: animateHover ? token('colors.highlight2') : dropHoverColorValue,
}}
/>
)
}
/** A drop-hover element that is rendered during drag-and-drop when it is possible to drop in a ThoughtDrop zone (next to a Thought). The canDrop and drop handlers can be found in the DropTarget components, DragAndDropThought and DragAndDropSubthoughts. */
const DropHoverIfVisible = ({
isHovering,
prevChildId,
simplePath,
}: {
isHovering: boolean
prevChildId?: ThoughtId
simplePath: SimplePath
}) => {
// true if a thought is being dragged over this drop hover
const showDropHover = useSelector(state => {
/** Returns true if hovering over current thought. */
const isThoughtHovering = () =>
state.hoveringPath &&
equalPath(parentOf(state.hoveringPath), parentOf(simplePath)) &&
state.hoverZone === DropThoughtZone.ThoughtDrop
/** Returns true if hovering over the current subthought. */
const isSubthoughtsHovering = () =>
state.hoveringPath &&
equalPath(state.hoveringPath, rootedParentOf(state, simplePath)) &&
state.hoverZone === DropThoughtZone.SubthoughtsDrop
const prevChild = prevChildId && getThoughtById(state, prevChildId)
if (!isThoughtHovering() && !isSubthoughtsHovering()) {
return false
}
const thought = getThoughtById(state, head(simplePath))
if (thought?.value === undefined) {
return false
}
// Don't show drop hover between contiguous selected thoughts
const contiguousDraggingThoughts = state.draggingThoughts.filter(draggingPath => {
// draggingThoughts are SimplePaths, so their siblings must always be resolved in normal view. Without
// showContexts:false, prevSibling infers the context view from the dragging thought's simple parent path, which
// in a cyclic context is the very path the context view is active on (dragging a/m~/a/x has simple parent a/m),
// and looks for x among the contexts of m instead of among the children of m.
const prev = prevSibling(state, draggingPath, { showContexts: false })
const prevPath = prev ? appendToPath(parentOf(draggingPath), prev.id) : null
return prev && state.draggingThoughts.some(simplePath => equalPath(prevPath, simplePath))
})
if (contiguousDraggingThoughts.some(draggingPath => equalPath(draggingPath, simplePath))) {
return false
}
// Typically we show the drop hover if the thought is being directly hovered over.
// However, when moving into a different context that is sorted, we need to show the drop hover on the sorted drop destination if the thought is hovered over any of the thoughts in the sorted context.
const sameContext = state.draggingThoughts.every(draggingPath =>
equalPath(rootedParentOf(state, draggingPath), rootedParentOf(state, simplePath)),
)
/** Returns true if the parent is sorted alphabetically. */
const isParentSorted = () => getSortPreference(state, thought.parentId).type === 'Alphabetical'
if (sameContext || !isParentSorted()) {
return testFlags.simulateDrag || isHovering
} else if (state.longPress !== LongPressState.DragInProgress) {
return false
}
const draggingThoughtValues = state.draggingThoughts
.map(draggingPath => getThoughtById(state, head(draggingPath))?.value)
.filter(nonNull)
// TODO: Show the first drop-hover with distance === 2. How to determine?
// const distance = state.cursor ? Math.max(0, Math.min(MAX_DISTANCE_FROM_CURSOR, state.cursor.length - depth!)) : 0
// Check if any of the dragging thoughts would be inserted at this position
return draggingThoughtValues.some(
draggingValue =>
// check if it's alphabetically previous to current thought
compareReasonable(draggingValue, thought.value) <= 0 &&
// check if it's alphabetically next to previous thought if it exists
(!prevChild || compareReasonable(draggingValue, prevChild.value) === 1),
)
})
// testFlags.pinDropHovers keeps the drop hover visible after it has been shown during the current drag
const showDropHoverPinned = usePinDropHover(showDropHover)
return showDropHoverPinned ? <DropHover simplePath={simplePath} /> : null
}
const DropHoverMemo = React.memo(DropHoverIfVisible)
DropHoverMemo.displayName = 'DropHover'
export default DropHoverMemo