import { ArrowDropDown, LayersOutlined, MoreVertOutlined, } from '@mui/icons-material' import { Box, Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, SvgIcon, ToggleButton, Typography, type SxProps, type Theme, } from '@mui/material' import deepmerge from 'deepmerge' import { useMemo, useState, type ComponentProps, type JSX, type MouseEvent, type PropsWithChildren, type ReactNode, } from 'react' import type { PickDeep } from 'type-fest' import { Tooltip } from '../tooltip/tooltip' import type { LassoToolsModes } from '../types' import { LassoToolsUIChip } from './chip' import { LASSO_TOOLS_LABELS } from './const' import { styles } from './styles' import type { LassoToolsComponentProps, OptionsChildrenProps } from './types' const EMPTY_VALUES: LassoToolsComponentProps['values'] = [] const EMPTY_PAPER_PROPS: NonNullable = {} /** * Provides a comprehensive floating interface for spatial selection and drawing operations on maps, supporting polygon, rectangle, circle, freehand lasso, and edit modes. * * @remarks * Supports customizable slots for action, options, chips, and secondary actions via compound component pattern (e.g., `LassoToolsUI.Action`, `LassoToolsUI.Chips`). * * @example * ```tsx * * ``` */ export function LassoToolsUI({ enabled, values = EMPTY_VALUES, actionProps, chipProps, labels, modes, modesMapping, modeSelected, PaperProps: { sx, ...PaperProps } = EMPTY_PAPER_PROPS, onActionToggle, onChipToggle, onDelete, onChangeMode, onAllChipToggle, onAllDelete, onLayerFilters, ChipsSlot = LassoToolsUI.Chips, ActionSlot = LassoToolsUI.Action, SecondaryActionsSlot = LassoToolsUI.SecondaryActions, OptionsSlot = LassoToolsUI.Options, }: LassoToolsComponentProps): JSX.Element { const slotArgs = { enabled, values, actionProps, chipProps, labels, modes, modesMapping, modeSelected, PaperProps, onActionToggle, onChipToggle, onDelete, onChangeMode, onAllChipToggle, onAllDelete, onLayerFilters, } return ( {!!ActionSlot && } {!!OptionsSlot && ( {(props) => { return }} )} {!!ChipsSlot && } {!!SecondaryActionsSlot && } ) } function LassoToolsUIAction({ actionProps, labels, enabled, children, onActionToggle, }: PropsWithChildren< Pick< LassoToolsComponentProps, 'actionProps' | 'enabled' | 'onActionToggle' > & { labels?: PickDeep['action'] } >) { const actionState = enabled ? 'active' : 'inactive' const actionLabel = labels?.tooltip?.[actionState] ?? LASSO_TOOLS_LABELS.action.tooltip[actionState] return ( onActionToggle(!enabled)} aria-label={actionLabel} selected={enabled} > {children} ) } const EMPTY_TRIGGER_PROPS: { Icon?: ReactNode; sx?: SxProps } = {} function Options({ TriggerProps = EMPTY_TRIGGER_PROPS, MenuProps, children, }: { TriggerProps?: { Icon?: ReactNode sx?: SxProps } MenuProps?: Partial> children: (props: OptionsChildrenProps) => JSX.Element }) { const { Icon, sx } = TriggerProps const IconNode = Icon ?? const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) const handleToggle = (event: MouseEvent) => { setAnchorEl(event.currentTarget) } const handleClose = () => { setAnchorEl(null) } return ( <> {IconNode}
{children({ onClose: handleClose, })}
) } function ModeList({ data, labels, children, }: { labels?: PickDeep['options'] data: LassoToolsComponentProps['modes'][keyof LassoToolsModes][] children: ReactNode }) { if (data.length <= 1) { return null } const modeTitle = labels?.mode?.title ?? LASSO_TOOLS_LABELS.options.mode.title return ( <> {modeTitle} {children} ) } function OptionsList({ data, modeSelected, labels, onChangeMode, onClose, }: Required> & { labels?: PickDeep['options'] data: (LassoToolsComponentProps['modes'] & LassoToolsComponentProps['modesMapping'])[keyof LassoToolsModes][] onClose: OptionsChildrenProps['onClose'] }) { const handleClick = ( e: MouseEvent, value: NonNullable<(typeof data)[number]>['value'], ) => { e.preventDefault() onChangeMode?.(value) onClose() } return data .filter((mode): mode is NonNullable => Boolean(mode)) .map((mode) => { const options = labels?.mode?.options ?? LASSO_TOOLS_LABELS.options.mode.options const label = options[mode.value] return ( handleClick(e, mode.value)} selected={mode.value === modeSelected} > {mode.icon} {label} ) }) } function SecondaryActionsWrapper({ values, labels, onAllChipToggle, onAllDelete, }: Partial< Omit< LassoToolsComponentProps, 'ChipsSlot' | 'ActionsSlot' | 'OptionsSlot' | 'SecondaryActionsSlot' > >) { if (!values?.length || values.length <= 1) { return null } const hasVisible = values?.some((value) => value.visible) const toggleAllLabel = hasVisible ? (labels?.actions?.toggleAll?.active ?? LASSO_TOOLS_LABELS.actions.toggleAll.active) : (labels?.actions?.toggleAll?.inactive ?? LASSO_TOOLS_LABELS.actions.toggleAll.inactive) const deleteAllLabel = labels?.actions?.deleteAll ?? LASSO_TOOLS_LABELS.actions.deleteAll return ( <> , sx: styles.options.more }} MenuProps={{ anchorOrigin: { vertical: 'bottom', horizontal: 'right', }, transformOrigin: { vertical: 'top', horizontal: 'right', }, }} > {(props) => { return ( <> { onAllChipToggle?.() props.onClose() }} > {toggleAllLabel} { onAllDelete?.() props.onClose() }} > {deleteAllLabel} ) }} ) } LassoToolsUI.Action = function ActionsWrapper({ modes, modesMapping, modeSelected, actionProps, enabled, labels, onActionToggle, }: Partial< Omit< LassoToolsComponentProps, 'ChipsSlot' | 'ActionsSlot' | 'OptionsSlot' | 'SecondaryActionsSlot' > > & { labels?: PickDeep['actions'] }) { const data = useMemo(() => { return deepmerge(modes ?? {}, modesMapping ?? {}) }, [modes, modesMapping]) const handleToggle: LassoToolsComponentProps['onActionToggle'] = (data) => { return onActionToggle?.(data) } const modeExists = modeSelected ? modeSelected in (modes ?? {}) : false const mode = ( modeExists ? modeSelected : Object.keys(data)[0] ) as keyof typeof data const modeSelectedValue = data[mode] return ( {modeSelectedValue?.icon} ) } LassoToolsUI.Chips = function ChipsWrapper({ values, labels, chipProps, onDelete, onChipToggle, onActionToggle, }: Partial< Omit< LassoToolsComponentProps, 'ChipsSlot' | 'ActionsSlot' | 'OptionsSlot' | 'SecondaryActionsSlot' > >) { const handleDelete: LassoToolsComponentProps['onDelete'] = (valueId) => { onActionToggle?.(false) return onDelete?.(valueId) } return ( !!values?.length && ( {values?.map((value) => ( onChipToggle?.(id, data)} /> ))} ) ) } LassoToolsUI.SecondaryActions = SecondaryActionsWrapper LassoToolsUI.Options = function OptionsWrapper({ values, modes, modesMapping, modeSelected, labels, onChangeMode, ...props }: Partial< Omit< LassoToolsComponentProps, 'ChipsSlot' | 'ActionsSlot' | 'OptionsSlot' | 'SecondaryActionsSlot' > > & OptionsChildrenProps) { const data = useMemo(() => { return deepmerge(modes ?? {}, modesMapping ?? {}) }, [modes, modesMapping]) const { edit, ..._modes } = data const modeExists = modeSelected ? modeSelected in (modes ?? {}) : false const mode = ( modeExists ? modeSelected : Object.keys(data)[0] ) as keyof typeof data const modesValues = Object.values(_modes) const handleChangeMode: LassoToolsComponentProps['onChangeMode'] = (data) => { return onChangeMode?.(data) } return ( <> {!!edit && ( <> )} {!!props.onLayerFilters && ( <> { props.onLayerFilters?.() props.onClose() }} > {labels?.options?.layerFilters ?? 'Layer filters'} )} ) }