[improvement] quick actions (#1922)
This PR: - improves the quick actions bar, enabling undo / redo actions when the eraser is selected. - for actions that effect selected shapes, calling the action when the select tool is not selected will select the select tool and run the action - actions that effect selected shapes are hidden from the menu when the select tool is not selected ### Change Type - [x] `major` ### Test Plan 1. Select the eraser tool, the undo / redo buttons should still be there. 1. Select two shapes 2. Select the draw tool 3. The menu should not display most options, e.g. cut or paste, but should display undo / redo 4. Press Shift+H 5. The shapes should not move, but the select tool should be selected again ### Release Notes - Improve the menu / kbds behavior when select tool is not active
This commit is contained in:
parent
386a2396d1
commit
b6ebe1e274
9 changed files with 387 additions and 251 deletions
|
@ -41,15 +41,20 @@ export async function setupPage(page: PlaywrightTestArgs['page']) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {
|
||||||
|
// delete everything
|
||||||
|
await page.keyboard.press('Control+a')
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
|
||||||
|
// create shapes
|
||||||
await page.keyboard.press('r')
|
await page.keyboard.press('r')
|
||||||
await page.mouse.click(200, 200)
|
await page.mouse.click(200, 200)
|
||||||
await page.keyboard.press('r')
|
await page.keyboard.press('r')
|
||||||
await page.mouse.click(200, 250)
|
await page.mouse.click(200, 250)
|
||||||
await page.keyboard.press('r')
|
await page.keyboard.press('r')
|
||||||
await page.mouse.click(250, 300)
|
await page.mouse.click(250, 300)
|
||||||
await page.evaluate(() => {
|
|
||||||
editor.selectNone()
|
// deselect everything
|
||||||
})
|
await page.evaluate(() => editor.selectNone())
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupPage(page: PlaywrightTestArgs['page']) {
|
export async function cleanupPage(page: PlaywrightTestArgs['page']) {
|
||||||
|
|
|
@ -64,8 +64,21 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Keyboard Shortcuts', () => {
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
page = await browser.newPage()
|
||||||
|
await setupPage(page)
|
||||||
|
|
||||||
|
// Make some shapes
|
||||||
|
await page.keyboard.press('r')
|
||||||
|
await page.mouse.click(100, 100)
|
||||||
|
await page.keyboard.press('r')
|
||||||
|
await page.mouse.click(250, 250)
|
||||||
|
await page.keyboard.press('v')
|
||||||
|
})
|
||||||
|
|
||||||
test.describe('actions', () => {
|
|
||||||
test('Zoom in', async () => {
|
test('Zoom in', async () => {
|
||||||
await page.keyboard.press('Control+=')
|
await page.keyboard.press('Control+=')
|
||||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
@ -134,8 +147,6 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
// data: { source: 'kbd' },
|
// data: { source: 'kbd' },
|
||||||
// })
|
// })
|
||||||
|
|
||||||
/* ------------------- Preferences ------------------ */
|
|
||||||
|
|
||||||
test('Toggle grid mode', async () => {
|
test('Toggle grid mode', async () => {
|
||||||
await page.keyboard.press("Control+'")
|
await page.keyboard.press("Control+'")
|
||||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
@ -159,6 +170,13 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
data: { source: 'kbd' },
|
data: { source: 'kbd' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Actions on shapes', () => {
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
page = await browser.newPage()
|
||||||
|
await setupPage(page)
|
||||||
|
})
|
||||||
|
|
||||||
/* -------------- Operations on Shapes -------------- */
|
/* -------------- Operations on Shapes -------------- */
|
||||||
|
|
||||||
|
@ -235,20 +253,6 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
data: { source: 'kbd' },
|
data: { source: 'kbd' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// delete — backspace
|
|
||||||
await page.keyboard.press('Backspace')
|
|
||||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
|
||||||
name: 'delete-shapes',
|
|
||||||
data: { source: 'kbd' },
|
|
||||||
})
|
|
||||||
|
|
||||||
// delete — ⌫
|
|
||||||
await page.keyboard.press('Delete')
|
|
||||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
|
||||||
name: 'delete-shapes',
|
|
||||||
data: { source: 'kbd' },
|
|
||||||
})
|
|
||||||
|
|
||||||
// align left — Alt+A
|
// align left — Alt+A
|
||||||
await page.keyboard.press('Alt+a')
|
await page.keyboard.press('Alt+a')
|
||||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
@ -277,6 +281,30 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
data: { operation: 'bottom', source: 'kbd' },
|
data: { operation: 'bottom', source: 'kbd' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// delete — backspace
|
||||||
|
await page.keyboard.press('Control+a') // selected
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
name: 'delete-shapes',
|
||||||
|
data: { source: 'kbd' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// delete — ⌫
|
||||||
|
|
||||||
|
// Make some shapes and select them
|
||||||
|
await page.keyboard.press('r')
|
||||||
|
await page.mouse.click(100, 100)
|
||||||
|
await page.keyboard.press('r')
|
||||||
|
await page.mouse.click(250, 250)
|
||||||
|
await page.keyboard.press('v')
|
||||||
|
await page.keyboard.press('Control+a')
|
||||||
|
|
||||||
|
await page.keyboard.press('Delete')
|
||||||
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
name: 'delete-shapes',
|
||||||
|
data: { source: 'kbd' },
|
||||||
|
})
|
||||||
|
|
||||||
/* ---------------------- Misc ---------------------- */
|
/* ---------------------- Misc ---------------------- */
|
||||||
|
|
||||||
// toggle lock
|
// toggle lock
|
||||||
|
@ -306,7 +334,6 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
test.describe('Context menu', async () => {
|
test.describe('Context menu', async () => {
|
||||||
test.beforeEach(async ({ browser }) => {
|
test.beforeEach(async ({ browser }) => {
|
||||||
|
|
|
@ -97,6 +97,7 @@ test.describe('Shape Tools', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('creates shapes clickable tools', async () => {
|
test('creates shapes clickable tools', async () => {
|
||||||
|
await page.keyboard.press('v')
|
||||||
await page.keyboard.press('Control+a')
|
await page.keyboard.press('Control+a')
|
||||||
await page.keyboard.press('Backspace')
|
await page.keyboard.press('Backspace')
|
||||||
expect(await getAllShapeTypes(page)).toEqual([])
|
expect(await getAllShapeTypes(page)).toEqual([])
|
||||||
|
@ -124,6 +125,7 @@ test.describe('Shape Tools', () => {
|
||||||
|
|
||||||
// Reset for next time
|
// Reset for next time
|
||||||
await page.mouse.click(0, 0) // to ensure we're not focused
|
await page.mouse.click(0, 0) // to ensure we're not focused
|
||||||
|
await page.keyboard.press('v') // go to the select tool
|
||||||
await page.keyboard.press('Control+a')
|
await page.keyboard.press('Control+a')
|
||||||
await page.keyboard.press('Backspace')
|
await page.keyboard.press('Backspace')
|
||||||
}
|
}
|
||||||
|
@ -162,6 +164,7 @@ test.describe('Shape Tools', () => {
|
||||||
|
|
||||||
// Reset for next time
|
// Reset for next time
|
||||||
await page.mouse.click(0, 0) // to ensure we're not focused
|
await page.mouse.click(0, 0) // to ensure we're not focused
|
||||||
|
await page.keyboard.press('v')
|
||||||
await page.keyboard.press('Control+a')
|
await page.keyboard.press('Control+a')
|
||||||
await page.keyboard.press('Backspace')
|
await page.keyboard.press('Backspace')
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,11 @@ export const DuplicateButton = track(function DuplicateButton() {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const action = actions['duplicate']
|
const action = actions['duplicate']
|
||||||
|
|
||||||
const noSelected = editor.selectedShapeIds.length <= 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={action.icon}
|
icon={action.icon}
|
||||||
onClick={() => action.onSelect('quick-actions')}
|
onClick={() => action.onSelect('quick-actions')}
|
||||||
disabled={noSelected}
|
disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
|
||||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||||
smallIcon
|
smallIcon
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -15,15 +15,13 @@ export const MenuZone = track(function MenuZone() {
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
const isReadonly = useReadonly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tlui-menu-zone">
|
<div className="tlui-menu-zone">
|
||||||
<div className="tlui-menu-zone__controls">
|
<div className="tlui-menu-zone__controls">
|
||||||
<Menu />
|
<Menu />
|
||||||
<div className="tlui-menu-zone__divider" />
|
<div className="tlui-menu-zone__divider" />
|
||||||
<PageMenu />
|
<PageMenu />
|
||||||
{breakpoint >= 6 && showQuickActions && (
|
{breakpoint >= 6 && !isReadonly && !editor.isInAny('hand', 'zoom') && (
|
||||||
<>
|
<>
|
||||||
<div className="tlui-menu-zone__divider" />
|
<div className="tlui-menu-zone__divider" />
|
||||||
<UndoButton />
|
<UndoButton />
|
||||||
|
|
|
@ -110,13 +110,12 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
<div className="tlui-toolbar">
|
<div className="tlui-toolbar">
|
||||||
<div className="tlui-toolbar__inner">
|
<div className="tlui-toolbar__inner">
|
||||||
<div className="tlui-toolbar__left">
|
<div className="tlui-toolbar__left">
|
||||||
{!isReadonly && (
|
{!isReadonly && breakpoint < 6 && !editor.isInAny('hand', 'zoom') && (
|
||||||
<div
|
<div
|
||||||
className={classNames('tlui-toolbar__extras', {
|
className={classNames('tlui-toolbar__extras', {
|
||||||
'tlui-toolbar__extras__hidden': !showExtraActions,
|
'tlui-toolbar__extras__hidden': !showExtraActions,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{breakpoint < 6 && (
|
|
||||||
<div className="tlui-toolbar__extras__controls">
|
<div className="tlui-toolbar__extras__controls">
|
||||||
<UndoButton />
|
<UndoButton />
|
||||||
<RedoButton />
|
<RedoButton />
|
||||||
|
@ -124,7 +123,6 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
<DuplicateButton />
|
<DuplicateButton />
|
||||||
<ActionsMenu />
|
<ActionsMenu />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<ToggleToolLockedButton activeToolId={activeToolId} />
|
<ToggleToolLockedButton activeToolId={activeToolId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -15,13 +15,11 @@ export const TrashButton = track(function TrashButton() {
|
||||||
|
|
||||||
if (isReadonly) return null
|
if (isReadonly) return null
|
||||||
|
|
||||||
const noSelected = editor.selectedShapeIds.length <= 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
icon={action.icon}
|
icon={action.icon}
|
||||||
onClick={() => action.onSelect('quick-actions')}
|
onClick={() => action.onSelect('quick-actions')}
|
||||||
disabled={noSelected}
|
disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
|
||||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||||
smallIcon
|
smallIcon
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -83,6 +83,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
|
|
||||||
// should this be a useMemo? looks like it doesn't actually deref any reactive values
|
// should this be a useMemo? looks like it doesn't actually deref any reactive values
|
||||||
const actions = React.useMemo<TLUiActionsContextType>(() => {
|
const actions = React.useMemo<TLUiActionsContextType>(() => {
|
||||||
|
function mustGoBackToSelectToolFirst() {
|
||||||
|
if (!editor.isIn('select')) {
|
||||||
|
editor.complete()
|
||||||
|
editor.setCurrentTool('select')
|
||||||
|
return false // false will still let the action happen, true will stop it
|
||||||
|
// todo: remove this return value once we're suuuuure
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSelectedShapes() {
|
||||||
|
return editor.selectedShapeIds.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
const actions = makeActions([
|
const actions = makeActions([
|
||||||
{
|
{
|
||||||
id: 'edit-link',
|
id: 'edit-link',
|
||||||
|
@ -90,6 +105,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'link',
|
icon: 'link',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('edit-link', { source })
|
trackEvent('edit-link', { source })
|
||||||
editor.mark('edit-link')
|
editor.mark('edit-link')
|
||||||
addDialog({ component: EditLinkDialog })
|
addDialog({ component: EditLinkDialog })
|
||||||
|
@ -209,6 +227,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
label: 'action.toggle-auto-size',
|
label: 'action.toggle-auto-size',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('toggle-auto-size', { source })
|
trackEvent('toggle-auto-size', { source })
|
||||||
editor.mark('toggling auto size')
|
editor.mark('toggling auto size')
|
||||||
editor.updateShapes(
|
editor.updateShapes(
|
||||||
|
@ -273,6 +294,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
label: 'action.convert-to-bookmark',
|
label: 'action.convert-to-bookmark',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.batch(() => {
|
||||||
trackEvent('convert-to-bookmark', { source })
|
trackEvent('convert-to-bookmark', { source })
|
||||||
const shapes = editor.selectedShapes
|
const shapes = editor.selectedShapes
|
||||||
|
@ -314,6 +338,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
label: 'action.convert-to-embed',
|
label: 'action.convert-to-embed',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('convert-to-embed', { source })
|
trackEvent('convert-to-embed', { source })
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.batch(() => {
|
||||||
|
@ -369,7 +396,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'duplicate',
|
icon: 'duplicate',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
if (editor.currentToolId !== 'select') return
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('duplicate-shapes', { source })
|
trackEvent('duplicate-shapes', { source })
|
||||||
const ids = editor.selectedShapeIds
|
const ids = editor.selectedShapeIds
|
||||||
const commonBounds = Box2d.Common(compact(ids.map((id) => editor.getShapePageBounds(id))))
|
const commonBounds = Box2d.Common(compact(ids.map((id) => editor.getShapePageBounds(id))))
|
||||||
|
@ -393,6 +422,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'ungroup',
|
icon: 'ungroup',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('ungroup-shapes', { source })
|
trackEvent('ungroup-shapes', { source })
|
||||||
editor.mark('ungroup')
|
editor.mark('ungroup')
|
||||||
editor.ungroupShapes(editor.selectedShapeIds)
|
editor.ungroupShapes(editor.selectedShapeIds)
|
||||||
|
@ -405,6 +437,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'group',
|
icon: 'group',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('group-shapes', { source })
|
trackEvent('group-shapes', { source })
|
||||||
const { onlySelectedShape } = editor
|
const { onlySelectedShape } = editor
|
||||||
if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) {
|
if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) {
|
||||||
|
@ -423,6 +458,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'align-left',
|
icon: 'align-left',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('align-shapes', { operation: 'left', source })
|
trackEvent('align-shapes', { operation: 'left', source })
|
||||||
editor.mark('align left')
|
editor.mark('align left')
|
||||||
editor.alignShapes(editor.selectedShapeIds, 'left')
|
editor.alignShapes(editor.selectedShapeIds, 'left')
|
||||||
|
@ -436,6 +474,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'align-center-horizontal',
|
icon: 'align-center-horizontal',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('align-shapes', { operation: 'center-horizontal', source })
|
trackEvent('align-shapes', { operation: 'center-horizontal', source })
|
||||||
editor.mark('align center horizontal')
|
editor.mark('align center horizontal')
|
||||||
editor.alignShapes(editor.selectedShapeIds, 'center-horizontal')
|
editor.alignShapes(editor.selectedShapeIds, 'center-horizontal')
|
||||||
|
@ -448,6 +489,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'align-right',
|
icon: 'align-right',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('align-shapes', { operation: 'right', source })
|
trackEvent('align-shapes', { operation: 'right', source })
|
||||||
editor.mark('align right')
|
editor.mark('align right')
|
||||||
editor.alignShapes(editor.selectedShapeIds, 'right')
|
editor.alignShapes(editor.selectedShapeIds, 'right')
|
||||||
|
@ -461,6 +505,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'align-center-vertical',
|
icon: 'align-center-vertical',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('align-shapes', { operation: 'center-vertical', source })
|
trackEvent('align-shapes', { operation: 'center-vertical', source })
|
||||||
editor.mark('align center vertical')
|
editor.mark('align center vertical')
|
||||||
editor.alignShapes(editor.selectedShapeIds, 'center-vertical')
|
editor.alignShapes(editor.selectedShapeIds, 'center-vertical')
|
||||||
|
@ -473,6 +520,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?W',
|
kbd: '?W',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('align-shapes', { operation: 'top', source })
|
trackEvent('align-shapes', { operation: 'top', source })
|
||||||
editor.mark('align top')
|
editor.mark('align top')
|
||||||
editor.alignShapes(editor.selectedShapeIds, 'top')
|
editor.alignShapes(editor.selectedShapeIds, 'top')
|
||||||
|
@ -485,6 +535,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?S',
|
kbd: '?S',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('align-shapes', { operation: 'bottom', source })
|
trackEvent('align-shapes', { operation: 'bottom', source })
|
||||||
editor.mark('align bottom')
|
editor.mark('align bottom')
|
||||||
editor.alignShapes(editor.selectedShapeIds, 'bottom')
|
editor.alignShapes(editor.selectedShapeIds, 'bottom')
|
||||||
|
@ -498,6 +551,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?!h',
|
kbd: '?!h',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('distribute-shapes', { operation: 'horizontal', source })
|
trackEvent('distribute-shapes', { operation: 'horizontal', source })
|
||||||
editor.mark('distribute horizontal')
|
editor.mark('distribute horizontal')
|
||||||
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
|
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
|
||||||
|
@ -511,6 +567,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?!V',
|
kbd: '?!V',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('distribute-shapes', { operation: 'vertical', source })
|
trackEvent('distribute-shapes', { operation: 'vertical', source })
|
||||||
editor.mark('distribute vertical')
|
editor.mark('distribute vertical')
|
||||||
editor.distributeShapes(editor.selectedShapeIds, 'vertical')
|
editor.distributeShapes(editor.selectedShapeIds, 'vertical')
|
||||||
|
@ -523,6 +582,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'stretch-horizontal',
|
icon: 'stretch-horizontal',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('stretch-shapes', { operation: 'horizontal', source })
|
trackEvent('stretch-shapes', { operation: 'horizontal', source })
|
||||||
editor.mark('stretch horizontal')
|
editor.mark('stretch horizontal')
|
||||||
editor.stretchShapes(editor.selectedShapeIds, 'horizontal')
|
editor.stretchShapes(editor.selectedShapeIds, 'horizontal')
|
||||||
|
@ -535,6 +597,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'stretch-vertical',
|
icon: 'stretch-vertical',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('stretch-shapes', { operation: 'vertical', source })
|
trackEvent('stretch-shapes', { operation: 'vertical', source })
|
||||||
editor.mark('stretch vertical')
|
editor.mark('stretch vertical')
|
||||||
editor.stretchShapes(editor.selectedShapeIds, 'vertical')
|
editor.stretchShapes(editor.selectedShapeIds, 'vertical')
|
||||||
|
@ -547,6 +612,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '!h',
|
kbd: '!h',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('flip-shapes', { operation: 'horizontal', source })
|
trackEvent('flip-shapes', { operation: 'horizontal', source })
|
||||||
editor.mark('flip horizontal')
|
editor.mark('flip horizontal')
|
||||||
editor.flipShapes(editor.selectedShapeIds, 'horizontal')
|
editor.flipShapes(editor.selectedShapeIds, 'horizontal')
|
||||||
|
@ -559,6 +627,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '!v',
|
kbd: '!v',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('flip-shapes', { operation: 'vertical', source })
|
trackEvent('flip-shapes', { operation: 'vertical', source })
|
||||||
editor.mark('flip vertical')
|
editor.mark('flip vertical')
|
||||||
editor.flipShapes(editor.selectedShapeIds, 'vertical')
|
editor.flipShapes(editor.selectedShapeIds, 'vertical')
|
||||||
|
@ -570,6 +641,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'pack',
|
icon: 'pack',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('pack-shapes', { source })
|
trackEvent('pack-shapes', { source })
|
||||||
editor.mark('pack')
|
editor.mark('pack')
|
||||||
editor.packShapes(editor.selectedShapeIds, 16)
|
editor.packShapes(editor.selectedShapeIds, 16)
|
||||||
|
@ -582,6 +656,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'stack-vertical',
|
icon: 'stack-vertical',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('stack-shapes', { operation: 'vertical', source })
|
trackEvent('stack-shapes', { operation: 'vertical', source })
|
||||||
editor.mark('stack-vertical')
|
editor.mark('stack-vertical')
|
||||||
editor.stackShapes(editor.selectedShapeIds, 'vertical', 16)
|
editor.stackShapes(editor.selectedShapeIds, 'vertical', 16)
|
||||||
|
@ -594,6 +671,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'stack-horizontal',
|
icon: 'stack-horizontal',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('stack-shapes', { operation: 'horizontal', source })
|
trackEvent('stack-shapes', { operation: 'horizontal', source })
|
||||||
editor.mark('stack-horizontal')
|
editor.mark('stack-horizontal')
|
||||||
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 16)
|
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 16)
|
||||||
|
@ -606,6 +686,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'bring-to-front',
|
icon: 'bring-to-front',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('reorder-shapes', { operation: 'toFront', source })
|
trackEvent('reorder-shapes', { operation: 'toFront', source })
|
||||||
editor.mark('bring to front')
|
editor.mark('bring to front')
|
||||||
editor.bringToFront(editor.selectedShapeIds)
|
editor.bringToFront(editor.selectedShapeIds)
|
||||||
|
@ -618,6 +701,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?]',
|
kbd: '?]',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('reorder-shapes', { operation: 'forward', source })
|
trackEvent('reorder-shapes', { operation: 'forward', source })
|
||||||
editor.mark('bring forward')
|
editor.mark('bring forward')
|
||||||
editor.bringForward(editor.selectedShapeIds)
|
editor.bringForward(editor.selectedShapeIds)
|
||||||
|
@ -630,6 +716,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?[',
|
kbd: '?[',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('reorder-shapes', { operation: 'backward', source })
|
trackEvent('reorder-shapes', { operation: 'backward', source })
|
||||||
editor.mark('send backward')
|
editor.mark('send backward')
|
||||||
editor.sendBackward(editor.selectedShapeIds)
|
editor.sendBackward(editor.selectedShapeIds)
|
||||||
|
@ -642,6 +731,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '[',
|
kbd: '[',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('reorder-shapes', { operation: 'toBack', source })
|
trackEvent('reorder-shapes', { operation: 'toBack', source })
|
||||||
editor.mark('send to back')
|
editor.mark('send to back')
|
||||||
editor.sendToBack(editor.selectedShapeIds)
|
editor.sendToBack(editor.selectedShapeIds)
|
||||||
|
@ -653,6 +745,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '$x',
|
kbd: '$x',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
editor.mark('cut')
|
editor.mark('cut')
|
||||||
cut(source)
|
cut(source)
|
||||||
},
|
},
|
||||||
|
@ -663,6 +758,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '$c',
|
kbd: '$c',
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
copy(source)
|
copy(source)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -688,11 +786,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
editor.batch(() => {
|
editor.batch(() => {
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('select-all-shapes', { source })
|
trackEvent('select-all-shapes', { source })
|
||||||
if (editor.currentToolId !== 'select') {
|
|
||||||
editor.cancel()
|
|
||||||
editor.setCurrentTool('select')
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.mark('select all kbd')
|
editor.mark('select all kbd')
|
||||||
editor.selectAll()
|
editor.selectAll()
|
||||||
|
@ -704,6 +800,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
label: 'action.select-none',
|
label: 'action.select-none',
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('select-none-shapes', { source })
|
trackEvent('select-none-shapes', { source })
|
||||||
editor.mark('select none')
|
editor.mark('select none')
|
||||||
editor.selectNone()
|
editor.selectNone()
|
||||||
|
@ -716,7 +815,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
if (editor.currentToolId !== 'select') return
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('delete-shapes', { source })
|
trackEvent('delete-shapes', { source })
|
||||||
editor.mark('delete')
|
editor.mark('delete')
|
||||||
editor.deleteShapes(editor.selectedShapeIds)
|
editor.deleteShapes(editor.selectedShapeIds)
|
||||||
|
@ -728,7 +829,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'rotate-cw',
|
icon: 'rotate-cw',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
if (editor.selectedShapeIds.length === 0) return
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('rotate-cw', { source })
|
trackEvent('rotate-cw', { source })
|
||||||
editor.mark('rotate-cw')
|
editor.mark('rotate-cw')
|
||||||
const offset = editor.selectionRotation % (TAU / 2)
|
const offset = editor.selectionRotation % (TAU / 2)
|
||||||
|
@ -742,7 +845,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
icon: 'rotate-ccw',
|
icon: 'rotate-ccw',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
if (editor.selectedShapeIds.length === 0) return
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('rotate-ccw', { source })
|
trackEvent('rotate-ccw', { source })
|
||||||
editor.mark('rotate-ccw')
|
editor.mark('rotate-ccw')
|
||||||
const offset = editor.selectionRotation % (TAU / 2)
|
const offset = editor.selectionRotation % (TAU / 2)
|
||||||
|
@ -797,6 +902,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '!2',
|
kbd: '!2',
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
|
if (!hasSelectedShapes()) return
|
||||||
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('zoom-to-selection', { source })
|
trackEvent('zoom-to-selection', { source })
|
||||||
editor.zoomToSelection({ duration: ANIMATION_MEDIUM_MS })
|
editor.zoomToSelection({ duration: ANIMATION_MEDIUM_MS })
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { Editor, TLBookmarkShape, TLEmbedShape, compact, useEditor, useValue } from '@tldraw/editor'
|
import { Editor, TLBookmarkShape, TLEmbedShape, useEditor, useValue } from '@tldraw/editor'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { getEmbedInfo } from '../../utils/embeds'
|
import { getEmbedInfo } from '../../utils/embeds'
|
||||||
import {
|
import {
|
||||||
TLUiMenuSchema,
|
TLUiMenuSchema,
|
||||||
|
compactMenuItems,
|
||||||
menuCustom,
|
menuCustom,
|
||||||
menuGroup,
|
menuGroup,
|
||||||
menuItem,
|
menuItem,
|
||||||
|
@ -108,7 +109,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
|
||||||
)
|
)
|
||||||
|
|
||||||
const menuSchema = useMemo<TLUiMenuSchema>(() => {
|
const menuSchema = useMemo<TLUiMenuSchema>(() => {
|
||||||
const menuSchema = compact([
|
const menuSchema = compactMenuItems([
|
||||||
menuGroup(
|
menuGroup(
|
||||||
'menu',
|
'menu',
|
||||||
menuSubmenu(
|
menuSubmenu(
|
||||||
|
|
Loading…
Reference in a new issue