Tldraw/packages/tldraw/src/components/TopPanel/PageMenu/PageMenu.tsx

223 wiersze
6.2 KiB
TypeScript

import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'
import * as React from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { Divider } from '~components/Primitives/Divider'
import { DMContent } from '~components/Primitives/DropdownMenu'
import { RowButton } from '~components/Primitives/RowButton'
import { SmallIcon } from '~components/Primitives/SmallIcon'
import { ToolButton } from '~components/Primitives/ToolButton'
import { useTldrawApp } from '~hooks'
import { styled } from '~styles'
import type { TDSnapshot } from '~types'
import { PageOptionsDialog } from '../PageOptionsDialog'
const sortedSelector = (s: TDSnapshot) =>
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
const currentPageNameSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].name
const currentPageIdSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].id
export function PageMenu() {
const app = useTldrawApp()
const intl = useIntl()
const rIsOpen = React.useRef(false)
const [isOpen, setIsOpen] = React.useState(false)
React.useEffect(() => {
if (rIsOpen.current !== isOpen) {
rIsOpen.current = isOpen
}
}, [isOpen])
const handleClose = React.useCallback(() => {
setIsOpen(false)
}, [setIsOpen])
const handleOpenChange = React.useCallback(
(isOpen: boolean) => {
if (rIsOpen.current !== isOpen) {
setIsOpen(isOpen)
}
},
[setIsOpen]
)
const currentPageName = app.useStore(currentPageNameSelector)
return (
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
<DropdownMenu.Trigger dir="ltr" asChild id="TD-Page">
<ToolButton variant="text">{currentPageName || intl.formatMessage({ id: 'page' })}</ToolButton>
</DropdownMenu.Trigger>
<DMContent variant="menu" align="start" sideOffset={4}>
{isOpen && <PageMenuContent onClose={handleClose} />}
</DMContent>
</DropdownMenu.Root>
)
}
function PageMenuContent({ onClose }: { onClose: () => void }) {
const app = useTldrawApp()
const intl = useIntl()
const sortedPages = app.useStore(sortedSelector)
const currentPageId = app.useStore(currentPageIdSelector)
const defaultPageName = intl.formatMessage({ id: 'page' })
const handleCreatePage = React.useCallback(() => {
const pageName =
defaultPageName + ' ' + (Object.keys(app.document.pages).length + 1)
app.createPage(undefined, pageName)
}, [app])
const handleChangePage = React.useCallback(
(id: string) => {
onClose()
app.changePage(id)
},
[app]
)
const [dragId, setDragId] = React.useState<null | string>(null)
const [dropIndex, setDropIndex] = React.useState<null | number>(null)
const handleDragStart = React.useCallback((ev: React.DragEvent<HTMLDivElement>) => {
setDragId(ev.currentTarget.id)
setDropIndex(sortedPages.findIndex((p) => p.id === ev.currentTarget.id))
ev.dataTransfer.effectAllowed = 'move'
}, [])
const handleDrag = React.useCallback(
(ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault()
let dropIndex = sortedPages.findIndex((p) => p.id === ev.currentTarget.id)
const rect = ev.currentTarget.getBoundingClientRect()
const ny = (ev.clientY - rect.top) / rect.height
dropIndex = ny < 0.5 ? dropIndex : dropIndex + 1
setDropIndex(dropIndex)
},
[dragId, sortedPages]
)
const handleDrop = React.useCallback(() => {
if (dragId !== null && dropIndex !== null) {
app.movePage(dragId, dropIndex)
}
setDragId(null)
setDropIndex(null)
}, [dragId, dropIndex])
return (
<>
<DropdownMenu.RadioGroup dir="ltr" value={currentPageId} onValueChange={handleChangePage}>
{sortedPages.map((page, i) => (
<ButtonWithOptions
key={page.id}
isDropAbove={i === dropIndex && i === 0}
isDropBelow={dropIndex !== null && i === dropIndex - 1}
>
<DropdownMenu.RadioItem
title={page.name || defaultPageName}
value={page.id}
key={page.id}
id={page.id}
asChild
onDragOver={handleDrag}
onDragStart={handleDragStart}
// onDrag={handleDrag}
onDrop={handleDrop}
draggable={true}
>
<PageButton>
<span id={page.id}>{page.name || defaultPageName}</span>
<DropdownMenu.ItemIndicator>
<SmallIcon>
<CheckIcon />
</SmallIcon>
</DropdownMenu.ItemIndicator>
</PageButton>
</DropdownMenu.RadioItem>
<PageOptionsDialog page={page} onClose={onClose} />
</ButtonWithOptions>
))}
</DropdownMenu.RadioGroup>
<Divider />
<DropdownMenu.Item onSelect={handleCreatePage} asChild>
<RowButton>
<span>
<FormattedMessage id="create.page" />
</span>
<SmallIcon>
<PlusIcon />
</SmallIcon>
</RowButton>
</DropdownMenu.Item>
</>
)
}
const ButtonWithOptions = styled('div', {
position: 'relative',
display: 'grid',
gridTemplateColumns: '1fr auto',
gridAutoFlow: 'column',
margin: 0,
'& > *[data-shy="true"]': {
opacity: 0,
},
'&:hover > *[data-shy="true"]': {
opacity: 1,
},
variants: {
isDropAbove: {
true: {
'&::after': {
content: '',
display: 'block',
position: 'absolute',
top: 0,
width: '100%',
height: '1px',
backgroundColor: '$selected',
zIndex: 999,
pointerEvents: 'none',
},
},
},
isDropBelow: {
true: {
'&::after': {
content: '',
display: 'block',
position: 'absolute',
width: '100%',
height: '1px',
top: '100%',
backgroundColor: '$selected',
zIndex: 999,
pointerEvents: 'none',
},
},
},
},
})
export const PageButton = styled(RowButton, {
minWidth: 128,
})