Tldraw/packages/ui/src/lib/components/ContextMenu.tsx

225 wiersze
6.8 KiB
TypeScript

import * as _ContextMenu from '@radix-ui/react-context-menu'
import { App, preventDefault, useApp, useContainer } from '@tldraw/editor'
import classNames from 'classnames'
import * as React from 'react'
import { useValue } from 'signia-react'
import { MenuChild } from '../hooks/menuHelpers'
import { useBreakpoint } from '../hooks/useBreakpoint'
import { useMenuClipboardEvents } from '../hooks/useClipboardEvents'
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
import { useReadonly } from '../hooks/useReadonly'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { MoveToPageMenu } from './MoveToPageMenu'
import { Button } from './primitives/Button'
import { Icon } from './primitives/Icon'
import { Kbd } from './primitives/Kbd'
/** @public */
export interface ContextMenuProps {
children: any
}
/** @public */
export const ContextMenu = function ContextMenu({ children }: { children: any }) {
const app = useApp()
const contextMenuSchema = useContextMenuSchema()
const handleOpenChange = useMenuIsOpen('context menu')
// If every item in the menu is readonly, then we don't want to show the menu
const isReadonly = useReadonly()
const noItemsToShow =
contextMenuSchema.length === 0 ||
(isReadonly && contextMenuSchema.every((item) => !item.readonlyOk))
const selectToolActive = useValue('isSelectToolActive', () => app.currentToolId === 'select', [
app,
])
const disabled = !selectToolActive || noItemsToShow
return (
<_ContextMenu.Root dir="ltr" onOpenChange={handleOpenChange}>
<_ContextMenu.Trigger
onContextMenu={disabled ? preventDefault : undefined}
dir="ltr"
disabled={disabled}
>
{children}
</_ContextMenu.Trigger>
<ContextMenuContent />
</_ContextMenu.Root>
)
}
function ContextMenuContent() {
const app = useApp()
const msg = useTranslation()
const menuSchema = useContextMenuSchema()
const handleSubOpenChange = useMenuIsOpen('context menu sub')
const isReadonly = useReadonly()
const { paste } = useMenuClipboardEvents()
const breakpoint = useBreakpoint()
const container = useContainer()
const [disableClicks, setDisableClicks] = React.useState(false)
function getContextMenuItem(app: App, item: MenuChild, parent: MenuChild | null, depth: number) {
if (isReadonly && !item.readonlyOk) return null
switch (item.type) {
case 'custom': {
switch (item.id) {
case 'MENU_PASTE': {
return (
<_ContextMenu.Item key={item.id}>
<Button
className="tlui-menu__button"
data-wd={`menu-item.${item.id}`}
kbd="$v"
label="action.paste"
disabled={item.disabled}
onClick={() => {
if (!app.isSafari || (app.isSafari && app.isIos)) {
navigator.clipboard.read().then((clipboardItems) => {
paste(clipboardItems, app.inputs.currentPagePoint)
})
}
}}
onMouseDown={() => {
if (app.isSafari && !app.isIos) {
// NOTE: This must be a onMouseDown for Safari/desktop, onClick doesn't work at the time of writing... 😒
navigator.clipboard.read().then((clipboardItems) => {
paste(clipboardItems, app.inputs.currentPagePoint)
})
}
}}
// onPointerUp={() => {
// if (app.isSafari && app.isIos) {
// // NOTE: This must be a onPointerUp for Safari/mobile, onClick doesn't work at the time of writing... 😒
// navigator.clipboard.read().then((clipboardItems) => {
// paste(clipboardItems, app.inputs.currentPagePoint)
// })
// }
// }}
/>
</_ContextMenu.Item>
)
}
case 'MOVE_TO_PAGE_MENU': {
return <MoveToPageMenu key={item.id} />
}
}
break
}
case 'group': {
return (
<_ContextMenu.Group
dir="ltr"
className={classNames('tlui-menu__group', {
'tlui-menu__group__small': parent?.type === 'submenu',
})}
data-wd={`menu-item.${item.id}`}
key={item.id}
>
{item.children.map((child) => getContextMenuItem(app, child, item, depth + 1))}
</_ContextMenu.Group>
)
}
case 'submenu': {
return (
<_ContextMenu.Sub key={item.id} onOpenChange={handleSubOpenChange}>
<_ContextMenu.SubTrigger dir="ltr" disabled={item.disabled} asChild>
<Button
className="tlui-menu__button"
label={item.label}
data-wd={`menu-item.${item.id}`}
icon="chevron-right"
/>
</_ContextMenu.SubTrigger>
<_ContextMenu.Portal container={container} dir="ltr">
<_ContextMenu.SubContent className="tlui-menu" sideOffset={-4} collisionPadding={4}>
{item.children.map((child) => getContextMenuItem(app, child, item, depth + 1))}
</_ContextMenu.SubContent>
</_ContextMenu.Portal>
</_ContextMenu.Sub>
)
}
case 'item': {
if (isReadonly && !item.readonlyOk) return null
const { id, checkbox, contextMenuLabel, label, onSelect, kbd, icon } = item.actionItem
const labelToUse = contextMenuLabel ?? label
const labelStr = labelToUse ? msg(labelToUse) : undefined
if (checkbox) {
// Item is in a checkbox group
return (
<_ContextMenu.CheckboxItem
key={id}
className="tlui-button tlui-menu__button tlui-menu__checkbox-item"
dir="ltr"
disabled={item.disabled}
onSelect={(e) => {
onSelect('context-menu')
preventDefault(e)
}}
title={labelStr ? labelStr : undefined}
checked={item.checked}
>
<div
className="tlui-menu__checkbox-item__check"
style={{
transformOrigin: '75% center',
transform: `scale(${item.checked ? 1 : 0.5})`,
opacity: item.checked ? 1 : 0.5,
}}
>
<Icon small icon={item.checked ? 'check' : 'checkbox-empty'} />
</div>
{labelStr && <span>{labelStr}</span>}
{kbd && <Kbd>{kbd}</Kbd>}
</_ContextMenu.CheckboxItem>
)
}
return (
<_ContextMenu.Item key={id} dir="ltr" asChild>
<Button
className="tlui-menu__button"
data-wd={`menu-item.${id}`}
kbd={kbd}
label={labelToUse}
disabled={item.disabled}
iconLeft={breakpoint < 3 && depth > 2 ? icon : undefined}
onClick={() => {
if (disableClicks) {
setDisableClicks(false)
} else {
onSelect('context-menu')
}
}}
/>
</_ContextMenu.Item>
)
}
}
}
return (
<_ContextMenu.Portal dir="ltr" container={container}>
<_ContextMenu.Content
className="tlui-menu scrollable"
alignOffset={-4}
collisionPadding={4}
onContextMenu={preventDefault}
>
{menuSchema.map((item) => getContextMenuItem(app, item, null, 0))}
</_ContextMenu.Content>
</_ContextMenu.Portal>
)
}