kopia lustrzana https://github.com/Tldraw/Tldraw
276 wiersze
8.2 KiB
TypeScript
276 wiersze
8.2 KiB
TypeScript
import { preventDefault, useApp } from '@tldraw/editor'
|
|
import classNames from 'classnames'
|
|
import React from 'react'
|
|
import { track, useValue } from 'signia-react'
|
|
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
|
import { useReadonly } from '../../hooks/useReadonly'
|
|
import { ToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema'
|
|
import { ToolItem } from '../../hooks/useTools'
|
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
|
import { ActionsMenu } from '../ActionsMenu'
|
|
import { DuplicateButton } from '../DuplicateButton'
|
|
import { MobileStylePanel } from '../MobileStylePanel'
|
|
import { RedoButton } from '../RedoButton'
|
|
import { TrashButton } from '../TrashButton'
|
|
import { UndoButton } from '../UndoButton'
|
|
import { Button } from '../primitives/Button'
|
|
import * as M from '../primitives/DropdownMenu'
|
|
import { kbdStr } from '../primitives/shared'
|
|
import { ToggleToolLockedButton } from './ToggleToolLockedButton'
|
|
|
|
/** @public */
|
|
export const Toolbar = function Toolbar() {
|
|
const app = useApp()
|
|
const msg = useTranslation()
|
|
const breakpoint = useBreakpoint()
|
|
|
|
const rMostRecentlyActiveDropdownItem = React.useRef<ToolbarItem | undefined>(undefined)
|
|
|
|
const isReadOnly = useReadonly()
|
|
const toolbarItems = useToolbarSchema()
|
|
|
|
const activeToolId = useValue('current tool id', () => app.currentToolId, [app])
|
|
|
|
const isHandTool = activeToolId === 'hand'
|
|
const geoState = useValue('geo', () => (app.props ? app.props.geo : undefined), [app])
|
|
|
|
const showEditingTools = !isReadOnly
|
|
const showExtraActions = !(isReadOnly || isHandTool)
|
|
|
|
const getTitle = (item: ToolItem) =>
|
|
item.label ? `${msg(item.label)} ${item.kbd ? kbdStr(item.kbd) : ''}` : ''
|
|
|
|
const activeToolbarItem = toolbarItems.find((item) => {
|
|
return isActiveToolItem(item.toolItem, activeToolId, geoState)
|
|
})
|
|
|
|
const { itemsInPanel, itemsInDropdown, dropdownFirstItem } = React.useMemo(() => {
|
|
const itemsInPanel: ToolbarItem[] = []
|
|
const itemsInDropdown: ToolbarItem[] = []
|
|
let dropdownFirstItem: ToolbarItem | undefined
|
|
|
|
const overflowIndex = Math.min(8, 5 + breakpoint)
|
|
|
|
for (let i = 4; i < toolbarItems.length; i++) {
|
|
const item = toolbarItems[i]
|
|
if (i < overflowIndex) {
|
|
// Items below the overflow index will always be in the panel
|
|
itemsInPanel.push(item)
|
|
} else {
|
|
// Items above will be in the dropdown menu unless the item
|
|
// is active (or was the most recently selected active item)
|
|
if (item === activeToolbarItem) {
|
|
// If the dropdown item is active, make it the dropdownFirstItem
|
|
dropdownFirstItem = item
|
|
}
|
|
// Otherwise, add it to the items in dropdown menu
|
|
itemsInDropdown.push(item)
|
|
}
|
|
}
|
|
|
|
if (dropdownFirstItem) {
|
|
// noop
|
|
} else {
|
|
// If we don't have a currently active dropdown item, use the most
|
|
// recently active dropdown item as the current dropdown first item.
|
|
|
|
// If haven't ever had a most recently active dropdown item, then
|
|
// make the first item in the dropdown menu the most recently
|
|
// active dropdown item.
|
|
if (!rMostRecentlyActiveDropdownItem.current) {
|
|
rMostRecentlyActiveDropdownItem.current = itemsInDropdown[0]
|
|
}
|
|
|
|
dropdownFirstItem = rMostRecentlyActiveDropdownItem.current
|
|
|
|
// If the most recently active dropdown item is no longer in the
|
|
// dropdown (because the breakpoint has changed) then make the
|
|
// first item in the dropdown menu the most recently active
|
|
// dropdown item.
|
|
if (!itemsInDropdown.includes(dropdownFirstItem)) {
|
|
dropdownFirstItem = itemsInDropdown[0]
|
|
}
|
|
}
|
|
|
|
// We want this ref set to remember which item from the current
|
|
// set of dropdown items was most recently active
|
|
rMostRecentlyActiveDropdownItem.current = dropdownFirstItem
|
|
|
|
if (itemsInDropdown.length <= 2) {
|
|
itemsInPanel.push(...itemsInDropdown)
|
|
itemsInDropdown.length = 0
|
|
}
|
|
|
|
return { itemsInPanel, itemsInDropdown, dropdownFirstItem }
|
|
}, [toolbarItems, activeToolbarItem, breakpoint])
|
|
|
|
return (
|
|
<div className="tlui-toolbar">
|
|
<div className="tlui-toolbar__inner">
|
|
<div className="tlui-toolbar__left">
|
|
{!isReadOnly && (
|
|
<div
|
|
className={classNames('tlui-toolbar__extras', {
|
|
'tlui-toolbar__extras__hidden': !showExtraActions,
|
|
})}
|
|
>
|
|
{breakpoint < 5 && (
|
|
<div className="tlui-toolbar__extras__controls">
|
|
<UndoButton />
|
|
<RedoButton />
|
|
<TrashButton />
|
|
<DuplicateButton />
|
|
<ActionsMenu />
|
|
</div>
|
|
)}
|
|
<ToggleToolLockedButton activeToolId={activeToolId} />
|
|
</div>
|
|
)}
|
|
<div
|
|
className={classNames('tlui-toolbar__tools', {
|
|
'tlui-toolbar__tools__mobile': breakpoint < 5,
|
|
})}
|
|
>
|
|
{/* Select / Hand */}
|
|
{toolbarItems.slice(0, 2).map(({ toolItem }) => {
|
|
return (
|
|
<ToolbarButton
|
|
key={toolItem.id}
|
|
item={toolItem}
|
|
title={getTitle(toolItem)}
|
|
isSelected={isActiveToolItem(toolItem, activeToolId, geoState)}
|
|
/>
|
|
)
|
|
})}
|
|
{showEditingTools && (
|
|
<>
|
|
{/* Draw / Eraser */}
|
|
<div className="tlui-toolbar__divider" />
|
|
{toolbarItems.slice(2, 4).map(({ toolItem }) => (
|
|
<ToolbarButton
|
|
key={toolItem.id}
|
|
item={toolItem}
|
|
title={getTitle(toolItem)}
|
|
isSelected={isActiveToolItem(toolItem, activeToolId, geoState)}
|
|
/>
|
|
))}
|
|
{/* Everything Else */}
|
|
<div className="tlui-toolbar__divider" />
|
|
{itemsInPanel.map(({ toolItem }) => (
|
|
<ToolbarButton
|
|
key={toolItem.id}
|
|
item={toolItem}
|
|
title={getTitle(toolItem)}
|
|
isSelected={isActiveToolItem(toolItem, activeToolId, geoState)}
|
|
/>
|
|
))}
|
|
{/* Overflowing Shapes */}
|
|
{itemsInDropdown.length ? (
|
|
<>
|
|
{/* Last selected (or first) item from the overflow */}
|
|
<ToolbarButton
|
|
key={dropdownFirstItem.toolItem.id}
|
|
item={dropdownFirstItem.toolItem}
|
|
title={getTitle(dropdownFirstItem.toolItem)}
|
|
isSelected={isActiveToolItem(
|
|
dropdownFirstItem.toolItem,
|
|
activeToolId,
|
|
geoState
|
|
)}
|
|
/>
|
|
{/* The dropdown to select everything else */}
|
|
<M.Root id="toolbar overflow" modal={false}>
|
|
<M.Trigger>
|
|
<Button
|
|
className="tlui-toolbar__tools__button tlui-toolbar__overflow"
|
|
icon="chevron-up"
|
|
data-wd="tools.more"
|
|
title={msg('tool-panel.more')}
|
|
/>
|
|
</M.Trigger>
|
|
<M.Content side="top" align="center">
|
|
<OverflowToolsContent toolbarItems={itemsInDropdown} />
|
|
</M.Content>
|
|
</M.Root>
|
|
</>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{breakpoint < 5 && !isReadOnly && (
|
|
<div className="tlui-toolbar__tools">
|
|
<MobileStylePanel />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const OverflowToolsContent = track(function OverflowToolsContent({
|
|
toolbarItems,
|
|
}: {
|
|
toolbarItems: ToolbarItem[]
|
|
}) {
|
|
const msg = useTranslation()
|
|
|
|
return (
|
|
<div className="tlui-button-grid__four tlui-button-grid__reverse">
|
|
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => {
|
|
return (
|
|
<M.Item
|
|
key={id}
|
|
className="tlui-button-grid__button"
|
|
data-wd={`tools.${id}`}
|
|
data-tool={id}
|
|
data-geo={meta?.geo ?? ''}
|
|
aria-label={label}
|
|
onClick={onSelect}
|
|
title={label ? `${msg(label)} ${kbd ? kbdStr(kbd) : ''}` : ''}
|
|
icon={icon}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
})
|
|
|
|
function ToolbarButton({
|
|
item,
|
|
title,
|
|
isSelected,
|
|
}: {
|
|
item: ToolItem
|
|
title: string
|
|
isSelected: boolean
|
|
}) {
|
|
return (
|
|
<Button
|
|
className="tlui-toolbar__tools__button"
|
|
data-wd={`tools.${item.id}`}
|
|
data-tool={item.id}
|
|
data-geo={item.meta?.geo ?? ''}
|
|
aria-label={item.label}
|
|
title={title}
|
|
icon={item.icon}
|
|
data-state={isSelected ? 'selected' : undefined}
|
|
onClick={item.onSelect}
|
|
onTouchStart={(e) => {
|
|
preventDefault(e)
|
|
item.onSelect()
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const isActiveToolItem = (
|
|
item: ToolItem,
|
|
activeToolId: string | undefined,
|
|
geoState: string | null | undefined
|
|
) => {
|
|
return item.meta?.geo
|
|
? activeToolId === 'geo' && geoState === item.meta?.geo
|
|
: activeToolId === item.id
|
|
}
|