[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']) {
|
||||
// delete everything
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// create shapes
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(200, 200)
|
||||
await page.keyboard.press('r')
|
||||
await page.mouse.click(200, 250)
|
||||
await page.keyboard.press('r')
|
||||
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']) {
|
||||
|
|
|
@ -64,246 +64,273 @@ test.describe('Keyboard Shortcuts', () => {
|
|||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('actions', () => {
|
||||
test('Zoom in', async () => {
|
||||
await page.keyboard.press('Control+=')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-in',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
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('Zoom in', async () => {
|
||||
await page.keyboard.press('Control+=')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-in',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Zoom out', async () => {
|
||||
await page.keyboard.press('Control+-')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-out',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Zoom to fit', async () => {
|
||||
await page.keyboard.press('Shift+1')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-to-fit',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Zoom to selection', async () => {
|
||||
await page.keyboard.press('Shift+2')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-to-selection',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Zoom to 100', async () => {
|
||||
await page.keyboard.press('Shift+0')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reset-zoom',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
/* ---------------------- Files --------------------- */
|
||||
|
||||
// new-project — Cmd+N
|
||||
// open — Cmd+O
|
||||
// save — Cmd+S
|
||||
// save-as — Cmd+Shift+S
|
||||
// upload-media — Cmd+I
|
||||
|
||||
/* -------------------- Clipboard ------------------- */
|
||||
|
||||
// await page.keyboard.press('Control+c')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'copy',
|
||||
// data: { source: 'kbd' },
|
||||
// })
|
||||
|
||||
// await page.keyboard.press('Control+v')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'paste',
|
||||
// data: { source: 'kbd' },
|
||||
// })
|
||||
|
||||
// await page.keyboard.press('Control+x')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'cut',
|
||||
// data: { source: 'kbd' },
|
||||
// })
|
||||
|
||||
test('Toggle grid mode', async () => {
|
||||
await page.keyboard.press("Control+'")
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-grid-mode',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Toggle dark mode', async () => {
|
||||
await page.keyboard.press('Control+/')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-dark-mode',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Toggle tool lock', async () => {
|
||||
await page.keyboard.press('q')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-tool-lock',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Actions on shapes', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
})
|
||||
|
||||
/* -------------- Operations on Shapes -------------- */
|
||||
|
||||
test('Operations on shapes', async () => {
|
||||
await setupPageWithShapes(page)
|
||||
|
||||
// select-all — Cmd+A
|
||||
await page.keyboard.press('Control+a')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'select-all-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
test('Zoom out', async () => {
|
||||
await page.keyboard.press('Control+-')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-out',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
// flip-h — Shift+H
|
||||
await page.keyboard.press('Shift+h')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'flip-shapes',
|
||||
data: { operation: 'horizontal', source: 'kbd' },
|
||||
})
|
||||
|
||||
test('Zoom to fit', async () => {
|
||||
await page.keyboard.press('Shift+1')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-to-fit',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
// flip-v — Shift+V
|
||||
await page.keyboard.press('Shift+v')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'flip-shapes',
|
||||
data: { operation: 'vertical', source: 'kbd' },
|
||||
})
|
||||
|
||||
test('Zoom to selection', async () => {
|
||||
await page.keyboard.press('Shift+2')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'zoom-to-selection',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
// move-to-front — ]
|
||||
await page.keyboard.press(']')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'toFront', source: 'kbd' },
|
||||
})
|
||||
|
||||
test('Zoom to 100', async () => {
|
||||
await page.keyboard.press('Shift+0')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reset-zoom',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
// move-forward — Alt+]
|
||||
await page.keyboard.press('Alt+]')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'forward', source: 'kbd' },
|
||||
})
|
||||
|
||||
/* ---------------------- Files --------------------- */
|
||||
// move-to-back — [
|
||||
await page.keyboard.press('[')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'toBack', source: 'kbd' },
|
||||
})
|
||||
|
||||
// new-project — Cmd+N
|
||||
// open — Cmd+O
|
||||
// save — Cmd+S
|
||||
// save-as — Cmd+Shift+S
|
||||
// upload-media — Cmd+I
|
||||
// move-backward — Alt+[
|
||||
await page.keyboard.press('Alt+[')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'backward', source: 'kbd' },
|
||||
})
|
||||
|
||||
/* -------------------- Clipboard ------------------- */
|
||||
// group — Cmd+G
|
||||
await page.keyboard.press('Control+g')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'group-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// await page.keyboard.press('Control+c')
|
||||
// ungroup — Cmd+Shift+G
|
||||
await page.keyboard.press('Control+Shift+g')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'ungroup-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// duplicate — Cmd+D
|
||||
await page.keyboard.press('Control+d')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'duplicate-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// align left — Alt+A
|
||||
await page.keyboard.press('Alt+a')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'left', source: 'kbd' },
|
||||
})
|
||||
|
||||
// align right — Alt+D
|
||||
await page.keyboard.press('Alt+d')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'right', source: 'kbd' },
|
||||
})
|
||||
|
||||
// align top — Alt+W
|
||||
await page.keyboard.press('Alt+w')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'top', source: 'kbd' },
|
||||
})
|
||||
|
||||
// align bottom — Alt+W'
|
||||
await page.keyboard.press('Alt+s')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
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 ---------------------- */
|
||||
|
||||
// toggle lock
|
||||
await page.keyboard.press('Shift+l')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-lock',
|
||||
})
|
||||
|
||||
// await page.keyboard.press('Control+i')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'copy',
|
||||
// data: { source: 'kbd' },
|
||||
// name: 'open-menu',
|
||||
// data: { source: 'dialog' },
|
||||
// })
|
||||
|
||||
// await page.keyboard.press('Control+v')
|
||||
// await page.keyboard.press('Control+u')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'paste',
|
||||
// data: { source: 'kbd' },
|
||||
// name: 'open-menu',
|
||||
// data: { source: 'dialog' },
|
||||
// })
|
||||
|
||||
// await page.keyboard.press('Control+x')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'cut',
|
||||
// data: { source: 'kbd' },
|
||||
// })
|
||||
/* --------------------- Export --------------------- */
|
||||
|
||||
/* ------------------- Preferences ------------------ */
|
||||
|
||||
test('Toggle grid mode', async () => {
|
||||
await page.keyboard.press("Control+'")
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-grid-mode',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Toggle dark mode', async () => {
|
||||
await page.keyboard.press('Control+/')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-dark-mode',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
test('Toggle tool lock', async () => {
|
||||
await page.keyboard.press('q')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-tool-lock',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
})
|
||||
|
||||
/* -------------- Operations on Shapes -------------- */
|
||||
|
||||
test('Operations on shapes', async () => {
|
||||
await setupPageWithShapes(page)
|
||||
|
||||
// select-all — Cmd+A
|
||||
await page.keyboard.press('Control+a')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'select-all-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// flip-h — Shift+H
|
||||
await page.keyboard.press('Shift+h')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'flip-shapes',
|
||||
data: { operation: 'horizontal', source: 'kbd' },
|
||||
})
|
||||
|
||||
// flip-v — Shift+V
|
||||
await page.keyboard.press('Shift+v')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'flip-shapes',
|
||||
data: { operation: 'vertical', source: 'kbd' },
|
||||
})
|
||||
|
||||
// move-to-front — ]
|
||||
await page.keyboard.press(']')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'toFront', source: 'kbd' },
|
||||
})
|
||||
|
||||
// move-forward — Alt+]
|
||||
await page.keyboard.press('Alt+]')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'forward', source: 'kbd' },
|
||||
})
|
||||
|
||||
// move-to-back — [
|
||||
await page.keyboard.press('[')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'toBack', source: 'kbd' },
|
||||
})
|
||||
|
||||
// move-backward — Alt+[
|
||||
await page.keyboard.press('Alt+[')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'reorder-shapes',
|
||||
data: { operation: 'backward', source: 'kbd' },
|
||||
})
|
||||
|
||||
// group — Cmd+G
|
||||
await page.keyboard.press('Control+g')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'group-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// ungroup — Cmd+Shift+G
|
||||
await page.keyboard.press('Control+Shift+g')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'ungroup-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
|
||||
// duplicate — Cmd+D
|
||||
await page.keyboard.press('Control+d')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'duplicate-shapes',
|
||||
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
|
||||
await page.keyboard.press('Alt+a')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'left', source: 'kbd' },
|
||||
})
|
||||
|
||||
// align right — Alt+D
|
||||
await page.keyboard.press('Alt+d')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'right', source: 'kbd' },
|
||||
})
|
||||
|
||||
// align top — Alt+W
|
||||
await page.keyboard.press('Alt+w')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'top', source: 'kbd' },
|
||||
})
|
||||
|
||||
// align bottom — Alt+W'
|
||||
await page.keyboard.press('Alt+s')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'align-shapes',
|
||||
data: { operation: 'bottom', source: 'kbd' },
|
||||
})
|
||||
|
||||
/* ---------------------- Misc ---------------------- */
|
||||
|
||||
// toggle lock
|
||||
await page.keyboard.press('Shift+l')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'toggle-lock',
|
||||
})
|
||||
|
||||
// await page.keyboard.press('Control+i')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'open-menu',
|
||||
// data: { source: 'dialog' },
|
||||
// })
|
||||
|
||||
// await page.keyboard.press('Control+u')
|
||||
// expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
// name: 'open-menu',
|
||||
// data: { source: 'dialog' },
|
||||
// })
|
||||
|
||||
/* --------------------- Export --------------------- */
|
||||
|
||||
await page.keyboard.press('Control+Shift+c')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'copy-as',
|
||||
data: { format: 'svg', source: 'kbd' },
|
||||
})
|
||||
await page.keyboard.press('Control+Shift+c')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'copy-as',
|
||||
data: { format: 'svg', source: 'kbd' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -97,6 +97,7 @@ test.describe('Shape Tools', () => {
|
|||
})
|
||||
|
||||
test('creates shapes clickable tools', async () => {
|
||||
await page.keyboard.press('v')
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
expect(await getAllShapeTypes(page)).toEqual([])
|
||||
|
@ -124,6 +125,7 @@ test.describe('Shape Tools', () => {
|
|||
|
||||
// Reset for next time
|
||||
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('Backspace')
|
||||
}
|
||||
|
@ -162,6 +164,7 @@ test.describe('Shape Tools', () => {
|
|||
|
||||
// Reset for next time
|
||||
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('Backspace')
|
||||
}
|
||||
|
|
|
@ -10,13 +10,11 @@ export const DuplicateButton = track(function DuplicateButton() {
|
|||
const msg = useTranslation()
|
||||
const action = actions['duplicate']
|
||||
|
||||
const noSelected = editor.selectedShapeIds.length <= 0
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={action.icon}
|
||||
onClick={() => action.onSelect('quick-actions')}
|
||||
disabled={noSelected}
|
||||
disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
|
||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||
smallIcon
|
||||
/>
|
||||
|
|
|
@ -15,15 +15,13 @@ export const MenuZone = track(function MenuZone() {
|
|||
const breakpoint = useBreakpoint()
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser')
|
||||
|
||||
return (
|
||||
<div className="tlui-menu-zone">
|
||||
<div className="tlui-menu-zone__controls">
|
||||
<Menu />
|
||||
<div className="tlui-menu-zone__divider" />
|
||||
<PageMenu />
|
||||
{breakpoint >= 6 && showQuickActions && (
|
||||
{breakpoint >= 6 && !isReadonly && !editor.isInAny('hand', 'zoom') && (
|
||||
<>
|
||||
<div className="tlui-menu-zone__divider" />
|
||||
<UndoButton />
|
||||
|
|
|
@ -110,21 +110,19 @@ export const Toolbar = memo(function Toolbar() {
|
|||
<div className="tlui-toolbar">
|
||||
<div className="tlui-toolbar__inner">
|
||||
<div className="tlui-toolbar__left">
|
||||
{!isReadonly && (
|
||||
{!isReadonly && breakpoint < 6 && !editor.isInAny('hand', 'zoom') && (
|
||||
<div
|
||||
className={classNames('tlui-toolbar__extras', {
|
||||
'tlui-toolbar__extras__hidden': !showExtraActions,
|
||||
})}
|
||||
>
|
||||
{breakpoint < 6 && (
|
||||
<div className="tlui-toolbar__extras__controls">
|
||||
<UndoButton />
|
||||
<RedoButton />
|
||||
<TrashButton />
|
||||
<DuplicateButton />
|
||||
<ActionsMenu />
|
||||
</div>
|
||||
)}
|
||||
<div className="tlui-toolbar__extras__controls">
|
||||
<UndoButton />
|
||||
<RedoButton />
|
||||
<TrashButton />
|
||||
<DuplicateButton />
|
||||
<ActionsMenu />
|
||||
</div>
|
||||
<ToggleToolLockedButton activeToolId={activeToolId} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -15,13 +15,11 @@ export const TrashButton = track(function TrashButton() {
|
|||
|
||||
if (isReadonly) return null
|
||||
|
||||
const noSelected = editor.selectedShapeIds.length <= 0
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={action.icon}
|
||||
onClick={() => action.onSelect('quick-actions')}
|
||||
disabled={noSelected}
|
||||
disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
|
||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||
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
|
||||
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([
|
||||
{
|
||||
id: 'edit-link',
|
||||
|
@ -90,6 +105,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'link',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('edit-link', { source })
|
||||
editor.mark('edit-link')
|
||||
addDialog({ component: EditLinkDialog })
|
||||
|
@ -209,6 +227,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.toggle-auto-size',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('toggle-auto-size', { source })
|
||||
editor.mark('toggling auto size')
|
||||
editor.updateShapes(
|
||||
|
@ -273,6 +294,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.convert-to-bookmark',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
editor.batch(() => {
|
||||
trackEvent('convert-to-bookmark', { source })
|
||||
const shapes = editor.selectedShapes
|
||||
|
@ -314,6 +338,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.convert-to-embed',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('convert-to-embed', { source })
|
||||
|
||||
editor.batch(() => {
|
||||
|
@ -369,7 +396,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'duplicate',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (editor.currentToolId !== 'select') return
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('duplicate-shapes', { source })
|
||||
const ids = editor.selectedShapeIds
|
||||
const commonBounds = Box2d.Common(compact(ids.map((id) => editor.getShapePageBounds(id))))
|
||||
|
@ -393,6 +422,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'ungroup',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('ungroup-shapes', { source })
|
||||
editor.mark('ungroup')
|
||||
editor.ungroupShapes(editor.selectedShapeIds)
|
||||
|
@ -405,6 +437,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'group',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('group-shapes', { source })
|
||||
const { onlySelectedShape } = editor
|
||||
if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) {
|
||||
|
@ -423,6 +458,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'align-left',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('align-shapes', { operation: 'left', source })
|
||||
editor.mark('align left')
|
||||
editor.alignShapes(editor.selectedShapeIds, 'left')
|
||||
|
@ -436,6 +474,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'align-center-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('align-shapes', { operation: 'center-horizontal', source })
|
||||
editor.mark('align center horizontal')
|
||||
editor.alignShapes(editor.selectedShapeIds, 'center-horizontal')
|
||||
|
@ -448,6 +489,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'align-right',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('align-shapes', { operation: 'right', source })
|
||||
editor.mark('align right')
|
||||
editor.alignShapes(editor.selectedShapeIds, 'right')
|
||||
|
@ -461,6 +505,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'align-center-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('align-shapes', { operation: 'center-vertical', source })
|
||||
editor.mark('align center vertical')
|
||||
editor.alignShapes(editor.selectedShapeIds, 'center-vertical')
|
||||
|
@ -473,6 +520,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?W',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('align-shapes', { operation: 'top', source })
|
||||
editor.mark('align top')
|
||||
editor.alignShapes(editor.selectedShapeIds, 'top')
|
||||
|
@ -485,6 +535,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?S',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('align-shapes', { operation: 'bottom', source })
|
||||
editor.mark('align bottom')
|
||||
editor.alignShapes(editor.selectedShapeIds, 'bottom')
|
||||
|
@ -498,6 +551,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?!h',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('distribute-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('distribute horizontal')
|
||||
editor.distributeShapes(editor.selectedShapeIds, 'horizontal')
|
||||
|
@ -511,6 +567,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?!V',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('distribute-shapes', { operation: 'vertical', source })
|
||||
editor.mark('distribute vertical')
|
||||
editor.distributeShapes(editor.selectedShapeIds, 'vertical')
|
||||
|
@ -523,6 +582,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'stretch-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('stretch-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('stretch horizontal')
|
||||
editor.stretchShapes(editor.selectedShapeIds, 'horizontal')
|
||||
|
@ -535,6 +597,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'stretch-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('stretch-shapes', { operation: 'vertical', source })
|
||||
editor.mark('stretch vertical')
|
||||
editor.stretchShapes(editor.selectedShapeIds, 'vertical')
|
||||
|
@ -547,6 +612,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '!h',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('flip-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('flip horizontal')
|
||||
editor.flipShapes(editor.selectedShapeIds, 'horizontal')
|
||||
|
@ -559,6 +627,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '!v',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('flip-shapes', { operation: 'vertical', source })
|
||||
editor.mark('flip vertical')
|
||||
editor.flipShapes(editor.selectedShapeIds, 'vertical')
|
||||
|
@ -570,6 +641,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'pack',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('pack-shapes', { source })
|
||||
editor.mark('pack')
|
||||
editor.packShapes(editor.selectedShapeIds, 16)
|
||||
|
@ -582,6 +656,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'stack-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('stack-shapes', { operation: 'vertical', source })
|
||||
editor.mark('stack-vertical')
|
||||
editor.stackShapes(editor.selectedShapeIds, 'vertical', 16)
|
||||
|
@ -594,6 +671,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'stack-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('stack-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('stack-horizontal')
|
||||
editor.stackShapes(editor.selectedShapeIds, 'horizontal', 16)
|
||||
|
@ -606,6 +686,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'bring-to-front',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('reorder-shapes', { operation: 'toFront', source })
|
||||
editor.mark('bring to front')
|
||||
editor.bringToFront(editor.selectedShapeIds)
|
||||
|
@ -618,6 +701,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?]',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('reorder-shapes', { operation: 'forward', source })
|
||||
editor.mark('bring forward')
|
||||
editor.bringForward(editor.selectedShapeIds)
|
||||
|
@ -630,6 +716,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?[',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('reorder-shapes', { operation: 'backward', source })
|
||||
editor.mark('send backward')
|
||||
editor.sendBackward(editor.selectedShapeIds)
|
||||
|
@ -642,6 +731,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '[',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('reorder-shapes', { operation: 'toBack', source })
|
||||
editor.mark('send to back')
|
||||
editor.sendToBack(editor.selectedShapeIds)
|
||||
|
@ -653,6 +745,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '$x',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
editor.mark('cut')
|
||||
cut(source)
|
||||
},
|
||||
|
@ -663,6 +758,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '$c',
|
||||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
copy(source)
|
||||
},
|
||||
},
|
||||
|
@ -688,11 +786,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
editor.batch(() => {
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('select-all-shapes', { source })
|
||||
if (editor.currentToolId !== 'select') {
|
||||
editor.cancel()
|
||||
editor.setCurrentTool('select')
|
||||
}
|
||||
|
||||
editor.mark('select all kbd')
|
||||
editor.selectAll()
|
||||
|
@ -704,6 +800,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.select-none',
|
||||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('select-none-shapes', { source })
|
||||
editor.mark('select none')
|
||||
editor.selectNone()
|
||||
|
@ -716,7 +815,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'trash',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (editor.currentToolId !== 'select') return
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('delete-shapes', { source })
|
||||
editor.mark('delete')
|
||||
editor.deleteShapes(editor.selectedShapeIds)
|
||||
|
@ -728,7 +829,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'rotate-cw',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (editor.selectedShapeIds.length === 0) return
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('rotate-cw', { source })
|
||||
editor.mark('rotate-cw')
|
||||
const offset = editor.selectionRotation % (TAU / 2)
|
||||
|
@ -742,7 +845,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'rotate-ccw',
|
||||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
if (editor.selectedShapeIds.length === 0) return
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('rotate-ccw', { source })
|
||||
editor.mark('rotate-ccw')
|
||||
const offset = editor.selectionRotation % (TAU / 2)
|
||||
|
@ -797,6 +902,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '!2',
|
||||
readonlyOk: true,
|
||||
onSelect(source) {
|
||||
if (!hasSelectedShapes()) return
|
||||
if (mustGoBackToSelectToolFirst()) return
|
||||
|
||||
trackEvent('zoom-to-selection', { source })
|
||||
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 { getEmbedInfo } from '../../utils/embeds'
|
||||
import {
|
||||
TLUiMenuSchema,
|
||||
compactMenuItems,
|
||||
menuCustom,
|
||||
menuGroup,
|
||||
menuItem,
|
||||
|
@ -108,7 +109,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
|
|||
)
|
||||
|
||||
const menuSchema = useMemo<TLUiMenuSchema>(() => {
|
||||
const menuSchema = compact([
|
||||
const menuSchema = compactMenuItems([
|
||||
menuGroup(
|
||||
'menu',
|
||||
menuSubmenu(
|
||||
|
|
Loading…
Reference in a new issue