optimized add item process

This commit is contained in:
Anton Tranelis 2025-10-12 12:33:08 +02:00
parent 108a9a0ba4
commit 2b638dffe5
2 changed files with 167 additions and 41 deletions

View File

@ -10,6 +10,8 @@ import { useLayers } from '#components/Map/hooks/useLayers'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react'
export default function AddButton({
triggerAction,
}: {
@ -24,9 +26,20 @@ export default function AddButton({
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (containerRef.current) {
DomEvent.disableClickPropagation(containerRef.current)
DomEvent.disableScrollPropagation(containerRef.current)
const container = containerRef.current
if (!container) return
DomEvent.disableClickPropagation(container)
DomEvent.disableScrollPropagation(container)
const stopPointerPropagation = (event: PointerEvent) => {
event.stopPropagation()
}
DomEvent.on(container, 'pointerdown pointerup pointermove', stopPointerPropagation)
return () => {
DomEvent.off(container, 'pointerdown pointerup pointermove', stopPointerPropagation)
}
}, [])
@ -44,6 +57,28 @@ export default function AddButton({
return canAdd
}
const stopPropagation = (
event: ReactMouseEvent<HTMLElement> | ReactTouchEvent<HTMLElement>,
): void => {
event.preventDefault()
event.stopPropagation()
if (
'nativeEvent' in event &&
typeof event.nativeEvent.stopImmediatePropagation === 'function'
) {
event.nativeEvent.stopImmediatePropagation()
}
}
const handleLayerSelect = (
event: ReactMouseEvent<HTMLButtonElement> | ReactTouchEvent<HTMLButtonElement>,
layer: (typeof layers)[number],
) => {
stopPropagation(event)
triggerAction(layer)
setIsOpen(false)
}
return (
<>
{canAddItems() ? (
@ -54,7 +89,23 @@ export default function AddButton({
<label
tabIndex={0}
className='tw:z-500 tw:btn tw:btn-circle tw:btn-lg tw:shadow tw:bg-base-100'
onClick={() => {
onMouseDown={(event) => {
stopPropagation(event)
}}
onMouseUp={(event) => {
stopPropagation(event)
}}
onClick={(event) => {
stopPropagation(event)
if (isMobile) {
setIsOpen(!isOpen)
}
}}
onTouchStart={(event) => {
stopPropagation(event)
}}
onTouchEnd={(event) => {
stopPropagation(event)
if (isMobile) {
setIsOpen(!isOpen)
}
@ -62,7 +113,10 @@ export default function AddButton({
>
<SVG src={PlusSVG} className='tw:h-5 tw:w-5' />
</label>
<ul tabIndex={0} className='tw:dropdown-content tw:pr-1 tw:list-none'>
<ul
tabIndex={0}
className='tw:dropdown-content tw:pr-1 tw:list-none tw:space-y-3 tw:pb-3'
>
{layers.map(
(layer) =>
layer.api?.createItem &&
@ -76,16 +130,22 @@ export default function AddButton({
>
<button
tabIndex={0}
className='tw:z-500 tw:border-0 tw:p-0 tw:mb-3 tw:w-10 tw:h-10 tw:cursor-pointer tw:rounded-full tw:mouse tw:drop-shadow-md tw:transition tw:ease-in tw:duration-200 tw:focus:outline-hidden tw:flex tw:items-center tw:justify-center'
className='tw:z-500 tw:border-0 tw:p-0 tw:w-10 tw:h-10 tw:cursor-pointer tw:rounded-full tw:mouse tw:drop-shadow-md tw:transition tw:ease-in tw:duration-200 tw:focus:outline-hidden tw:flex tw:items-center tw:justify-center'
style={{ backgroundColor: layer.menuColor || '#777' }}
onClick={() => {
triggerAction(layer)
setIsOpen(false)
onMouseDown={(event) => {
stopPropagation(event)
}}
onTouchEnd={(e) => {
triggerAction(layer)
setIsOpen(false)
e.preventDefault()
onMouseUp={(event) => {
stopPropagation(event)
}}
onClick={(event) => {
handleLayerSelect(event, layer)
}}
onTouchStart={(event) => {
stopPropagation(event)
}}
onTouchEnd={(event) => {
handleLayerSelect(event, layer)
}}
>
<img

View File

@ -1,12 +1,18 @@
import { useEffect, useRef } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { toast } from 'react-toastify'
import { SVG } from '#components/AppShell'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import type { Item } from '#types/Item'
import type { LayerProps } from '#types/LayerProps'
import type { Dispatch, SetStateAction } from 'react'
const isItemSelection = (value: Item | LayerProps): value is Item => 'layer' in value
interface SelectPositionToastProps {
selectNewItemPosition: Item | LayerProps | null
setSelectNewItemPosition: React.Dispatch<React.SetStateAction<Item | LayerProps | null>>
setSelectNewItemPosition: Dispatch<SetStateAction<Item | LayerProps | null>>
}
export const SelectPositionToast = ({
@ -14,12 +20,14 @@ export const SelectPositionToast = ({
setSelectNewItemPosition,
}: SelectPositionToastProps) => {
const toastIdRef = useRef<string | number | null>(null)
const toastId = 'select-position-toast'
const appState = useAppState()
// Escape-Key Listener
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && selectNewItemPosition) {
toast.dismiss('select-position-toast')
toast.dismiss(toastId)
toastIdRef.current = null
setSelectNewItemPosition(null)
}
@ -29,48 +37,106 @@ export const SelectPositionToast = ({
return () => window.removeEventListener('keydown', handleEscape)
}, [selectNewItemPosition, setSelectNewItemPosition])
useEffect(() => {
if (selectNewItemPosition && !toastIdRef.current) {
let message = ''
if ('layer' in selectNewItemPosition) {
message = `Select the new position of ${selectNewItemPosition.name} on the map!`
} else if ('markerIcon' in selectNewItemPosition) {
message = 'Select the position on the map!'
}
const toastContent = useMemo(() => {
if (!selectNewItemPosition) return null
const CloseButton = () => (
const itemSelection = isItemSelection(selectNewItemPosition)
const layer: LayerProps | null = itemSelection
? (selectNewItemPosition.layer ?? null)
: selectNewItemPosition
const markerIcon = itemSelection
? (selectNewItemPosition.layer?.markerIcon ?? selectNewItemPosition.markerIcon)
: selectNewItemPosition.markerIcon
const message = itemSelection
? `Select the new position of ${selectNewItemPosition.name} on the map!`
: 'Select the position on the map!'
const dismissToast = () => {
toast.dismiss(toastId)
toastIdRef.current = null
setSelectNewItemPosition(null)
}
const assetsApiUrl = appState.assetsApi.url
const assetsBaseUrl: string | undefined = assetsApiUrl.length > 0 ? assetsApiUrl : undefined
const resolveColor = (...candidates: (string | null | undefined)[]): string => {
for (const candidate of candidates) {
if (typeof candidate === 'string' && candidate.length > 0) {
return candidate
}
}
return '#777'
}
const itemColor =
itemSelection && typeof selectNewItemPosition.color === 'string'
? selectNewItemPosition.color
: undefined
const itemLayerColor =
itemSelection && typeof selectNewItemPosition.layer?.menuColor === 'string'
? selectNewItemPosition.layer.menuColor
: undefined
const layerMenuColor = layer?.menuColor
const baseLayerColor = typeof layerMenuColor === 'string' ? layerMenuColor : undefined
const backgroundColor = resolveColor(itemColor, itemLayerColor, baseLayerColor)
const iconSrc: string | undefined =
markerIcon?.image != null
? assetsBaseUrl
? `${assetsBaseUrl}${markerIcon.image}`
: markerIcon.image
: undefined
return (
<div className='tw:relative'>
<div className='tw:flex tw:flex-row tw:items-center'>
<div
className='tw:flex tw:items-center tw:gap-3 tw:p-2 tw:rounded-selector tw:text-white tw:mr-2'
style={{ backgroundColor }}
>
{iconSrc && <SVG src={iconSrc} className='tw:h-4 tw:w-4 tw:object-contain' />}
</div>
<div className='tw:flex tw:flex-col tw:gap-0.5'>
<span className='tw:text-sm'>{message}</span>
</div>
</div>
<button
onClick={() => {
toast.dismiss('select-position-toast')
toastIdRef.current = null
setSelectNewItemPosition(null)
}}
onClick={dismissToast}
className='tw:btn tw:btn-sm tw:btn-ghost tw:btn-circle tw:absolute tw:top-0 tw:right-0'
>
</button>
)
</div>
)
}, [appState.assetsApi.url, selectNewItemPosition, setSelectNewItemPosition, toastId])
toastIdRef.current = toast(
<div>
{message}
<CloseButton />
</div>,
{
toastId: 'select-position-toast',
useEffect(() => {
if (selectNewItemPosition && toastContent) {
if (!toastIdRef.current) {
toastIdRef.current = toast(toastContent, {
toastId,
autoClose: false,
closeButton: false,
closeOnClick: false,
draggable: false,
},
)
})
} else {
toast.update(toastId, {
render: toastContent,
autoClose: false,
closeButton: false,
closeOnClick: false,
draggable: false,
})
}
}
if (!selectNewItemPosition && toastIdRef.current) {
toast.dismiss(toastIdRef.current)
toastIdRef.current = null
}
}, [selectNewItemPosition, setSelectNewItemPosition])
}, [selectNewItemPosition, toastContent, toastId])
return null
}