omnivore/packages/web/components/elements/LabelsPicker.tsx

270 lines
7.4 KiB
TypeScript
Raw Normal View History

2023-06-16 04:55:46 +00:00
import AutosizeInput from 'react-input-autosize'
import { Box, SpanBox } from './LayoutPrimitives'
2023-06-21 11:59:32 +00:00
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2023-06-16 04:55:46 +00:00
import { Label } from '../../lib/networking/fragments/labelFragment'
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
import { isTouchScreenDevice } from '../../lib/deviceType'
2023-06-21 11:59:32 +00:00
import { EditLabelChip } from './EditLabelChip'
2023-06-20 05:36:36 +00:00
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
2023-06-21 11:59:32 +00:00
import { EditLabelChipStack } from './EditLabelChipStack'
2023-06-21 12:46:09 +00:00
const MaxUnstackedLabels = 7
2023-06-16 04:55:46 +00:00
type LabelsPickerProps = {
selectedLabels: Label[]
focused: boolean
inputValue: string
setInputValue: (value: string) => void
clearInputState: () => void
2023-06-16 04:55:46 +00:00
onFocus?: () => void
2023-06-20 05:36:36 +00:00
dispatchLabels: LabelsDispatcher
deleteLastLabel: () => void
selectOrCreateLabel: (value: string) => void
tabCount: number
setTabCount: (count: number) => void
tabStartValue: string
setTabStartValue: (value: string) => void
highlightLastLabel: boolean
setHighlightLastLabel: (set: boolean) => void
2023-06-16 04:55:46 +00:00
}
export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
const inputRef = useRef<HTMLInputElement | null>()
const availableLabels = useGetLabelsQuery()
2023-06-21 11:59:32 +00:00
const [isStackExpanded, setIsStackExpanded] = useState(false)
2023-06-20 05:36:36 +00:00
const {
focused,
inputValue,
tabCount,
tabStartValue,
selectedLabels,
setInputValue,
setTabCount,
setTabStartValue,
} = props
2023-06-16 04:55:46 +00:00
useEffect(() => {
2023-06-20 05:36:36 +00:00
if (!isTouchScreenDevice() && focused && inputRef.current) {
2023-06-16 04:55:46 +00:00
inputRef.current.focus()
}
}, [inputRef.current, focused])
2023-06-16 04:55:46 +00:00
const autoComplete = useCallback(() => {
2023-06-20 05:36:36 +00:00
const lowerCasedValue = inputValue.toLowerCase()
2023-06-16 04:55:46 +00:00
if (lowerCasedValue.length < 1) {
return
}
2023-06-20 05:36:36 +00:00
let _tabCount = tabCount
let _tabStartValue = tabStartValue.toLowerCase()
2023-06-16 04:55:46 +00:00
if (_tabCount === -1) {
_tabCount = 0
_tabStartValue = lowerCasedValue
2023-06-20 05:36:36 +00:00
setTabCount(0)
setTabStartValue(lowerCasedValue)
2023-06-16 04:55:46 +00:00
} else {
2023-06-20 05:36:36 +00:00
_tabCount = tabCount + 1
setTabCount(_tabCount)
2023-06-16 04:55:46 +00:00
}
const matches = availableLabels.labels.filter((l) =>
l.name.toLowerCase().startsWith(_tabStartValue)
)
if (_tabCount < matches.length) {
2023-06-20 05:36:36 +00:00
setInputValue(matches[_tabCount].name)
2023-06-16 04:55:46 +00:00
} else if (matches.length > 0) {
2023-06-20 05:36:36 +00:00
setTabCount(0)
setInputValue(matches[0].name)
2023-06-16 04:55:46 +00:00
}
2023-06-20 05:36:36 +00:00
}, [
inputValue,
availableLabels,
tabCount,
tabStartValue,
setInputValue,
setTabCount,
setTabStartValue,
])
2023-06-16 04:55:46 +00:00
const clearTabState = useCallback(() => {
2023-06-20 05:36:36 +00:00
setTabCount(-1)
setTabStartValue('')
}, [setTabCount, setTabStartValue])
2023-06-16 04:55:46 +00:00
const isEmpty = useMemo(() => {
2023-06-20 05:36:36 +00:00
return selectedLabels.length === 0 && inputValue.length === 0
}, [inputValue, selectedLabels])
2023-06-16 04:55:46 +00:00
2023-06-21 11:59:32 +00:00
const isStacked = useMemo(() => {
return selectedLabels.length > MaxUnstackedLabels && !isStackExpanded
}, [selectedLabels.length, isStackExpanded])
2023-06-16 04:55:46 +00:00
return (
<Box
css={{
display: 'inline-block',
2023-06-20 09:06:18 +00:00
bg: '$thBackground2', // '#3D3D3D' '#D9D9D9',
2023-06-16 04:55:46 +00:00
border: '1px transparent solid',
borderRadius: '6px',
2023-06-19 08:28:41 +00:00
verticalAlign: 'center',
2023-06-16 04:55:46 +00:00
padding: '5px',
lineHeight: '2',
width: '100%',
cursor: 'text',
2023-06-20 09:06:18 +00:00
color: '$thTextContrast2',
2023-06-16 04:55:46 +00:00
fontSize: '12px',
fontFamily: '$inter',
input: {
all: 'unset',
left: '0px',
outline: 'none',
borderStyle: 'none',
marginLeft: '2px',
},
'&:focus-within': {
outline: 'none',
border: '1px solid #898989',
2023-06-16 04:55:46 +00:00
},
'>span': {
marginTop: '0px',
marginBottom: '0px',
},
'>input': {
fontSize: '16px',
},
2023-06-16 04:55:46 +00:00
}}
onMouseDown={(event) => {
inputRef.current?.focus()
props.setHighlightLastLabel(false)
2023-06-16 04:55:46 +00:00
inputRef.current?.setSelectionRange(
inputRef.current?.value.length,
inputRef.current?.value.length
)
event.preventDefault()
}}
onDoubleClick={(event) => {
inputRef.current?.focus()
props.setHighlightLastLabel(false)
2023-06-16 04:55:46 +00:00
inputRef.current?.setSelectionRange(0, inputRef.current?.value.length)
2023-06-20 05:36:36 +00:00
event.preventDefault()
2023-06-16 04:55:46 +00:00
}}
>
2023-06-21 11:59:32 +00:00
{isStacked ? (
<EditLabelChipStack
labels={selectedLabels}
setExpanded={(expanded: boolean) => {
setIsStackExpanded(true)
}}
2023-06-21 11:59:32 +00:00
isSelected={props.highlightLastLabel}
2023-06-16 04:55:46 +00:00
/>
2023-06-21 11:59:32 +00:00
) : (
props.selectedLabels.map((label, idx) => (
<EditLabelChip
key={label.id}
text={label.name}
color={label.color}
isSelected={
props.highlightLastLabel && idx == props.selectedLabels.length - 1
}
xAction={() => {
const idx = props.selectedLabels.findIndex(
(l) => l.id == label.id
)
if (idx !== -1) {
const _selectedLabels = props.selectedLabels
_selectedLabels.splice(idx, 1)
props.dispatchLabels({
type: 'SAVE',
labels: [..._selectedLabels],
})
}
}}
/>
))
)}
2023-06-19 08:28:41 +00:00
<SpanBox
css={{
display: 'inline-flex',
height: '24px',
paddingBottom: '20px',
transform: `translateY(-2px)`,
2023-06-16 04:55:46 +00:00
}}
2023-06-19 08:28:41 +00:00
>
<AutosizeInput
placeholder={isEmpty ? 'Add Labels' : undefined}
2023-06-19 08:28:41 +00:00
inputRef={(ref) => {
inputRef.current = ref
}}
inputStyle={{
fontSize: '16px',
minWidth:
props.inputValue.length == 0 && props.selectedLabels.length == 0
? '100px'
: '2px',
}}
2023-06-19 08:28:41 +00:00
onFocus={() => {
if (props.onFocus) {
props.onFocus()
}
}}
minWidth="2px"
maxLength={48}
value={props.inputValue}
2023-06-19 08:28:41 +00:00
onClick={(event) => {
event.stopPropagation()
}}
onKeyUp={(event) => {
switch (event.key) {
case 'Escape':
props.clearInputState()
2023-06-19 08:28:41 +00:00
break
case 'Enter':
2023-06-21 11:59:32 +00:00
if (isStacked && props.highlightLastLabel) {
setIsStackExpanded(true)
props.setHighlightLastLabel(false)
} else {
props.selectOrCreateLabel(props.inputValue)
}
2023-06-16 04:55:46 +00:00
event.preventDefault()
2023-06-19 08:28:41 +00:00
break
}
}}
onKeyDown={(event) => {
switch (event.key) {
case 'Tab':
autoComplete()
event.preventDefault()
break
case 'Delete':
case 'Backspace':
clearTabState()
if (props.inputValue.length === 0) {
2023-06-21 11:59:32 +00:00
if (isStacked && props.highlightLastLabel) {
setIsStackExpanded(true)
props.setHighlightLastLabel(false)
} else {
props.deleteLastLabel()
}
2023-06-19 08:28:41 +00:00
event.preventDefault()
}
break
}
}}
onChange={function (event) {
props.setInputValue(event.target.value)
2023-06-19 08:28:41 +00:00
}}
/>
</SpanBox>
2023-06-16 04:55:46 +00:00
</Box>
)
}