From 9c45e0a5a5294b2ee5df5adf519cbd3267565251 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 16 Aug 2021 08:49:31 +0100 Subject: [PATCH] Starts on menu, page panel --- .../src/components/binding/binding.test.tsx | 2 +- .../src/components/bounds/bounds.test.tsx | 2 +- .../core/src/components/brush/brush.test.tsx | 2 +- .../src/components/canvas/canvas.test.tsx | 2 +- .../core/src/components/defs/defs.test.tsx | 2 +- .../error-fallback/error-fallback.test.tsx | 2 +- .../src/components/handles/handles.test.tsx | 2 +- .../core/src/components/page/page.test.tsx | 2 +- .../src/components/renderer/renderer.test.tsx | 2 +- .../shape-indicator/shape-indicator.test.tsx | 2 +- .../core/src/components/shape/shape.test.tsx | 2 +- packages/tldraw/package.json | 3 +- .../context-menu/context-menu.test.tsx | 2 +- packages/tldraw/src/components/menu/index.ts | 0 .../tldraw/src/components/menu/menu.test.tsx | 9 ++ packages/tldraw/src/components/menu/menu.tsx | 126 ++++++++++++++++++ .../src/components/menu/preferences.tsx | 20 +++ .../components/page-options-dialog/index.ts | 1 + .../page-options-dialog.test.tsx | 9 ++ .../page-options-dialog.tsx | 122 +++++++++++++++++ .../tldraw/src/components/page-panel/index.ts | 1 + .../components/page-panel/page-panel.test.tsx | 9 ++ .../src/components/page-panel/page-panel.tsx | 110 +++++++++++++++ .../style-panel/style-panel.test.tsx | 2 +- .../src/components/tldraw/tldraw.test.tsx | 2 +- .../tldraw/src/components/tldraw/tldraw.tsx | 9 +- .../tools-panel/tools-panel.test.tsx | 2 +- packages/tldraw/src/hooks/useTheme.ts | 1 + .../change-page/change-page.command.spec.ts | 28 ++++ .../change-page/change-page.command.ts | 10 ++ .../src/state/command/change-page/index.ts | 1 + .../create-page/create-page.command.spec.ts | 26 ++++ .../create-page/create-page.command.ts | 10 ++ .../src/state/command/create-page/index.ts | 1 + .../delete-page/delete-page.command.spec.ts | 28 ++++ .../delete-page/delete-page.command.ts | 10 ++ .../src/state/command/delete-page/index.ts | 1 + .../duplicate-page.command.spec.ts | 24 ++++ .../duplicate-page/duplicate-page.command.ts | 10 ++ .../src/state/command/duplicate-page/index.ts | 1 + .../src/state/command/rename-page/index.ts | 1 + .../rename-page/rename-page.command.spec.ts | 25 ++++ .../rename-page/rename-page.command.ts | 10 ++ packages/tldraw/src/state/tlstate.ts | 106 +++++++++++++-- packages/tldraw/src/types.ts | 4 +- yarn.lock | 100 ++++++++++++++ 46 files changed, 811 insertions(+), 35 deletions(-) create mode 100644 packages/tldraw/src/components/menu/index.ts create mode 100644 packages/tldraw/src/components/menu/menu.test.tsx create mode 100644 packages/tldraw/src/components/menu/menu.tsx create mode 100644 packages/tldraw/src/components/menu/preferences.tsx create mode 100644 packages/tldraw/src/components/page-options-dialog/index.ts create mode 100644 packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx create mode 100644 packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx create mode 100644 packages/tldraw/src/components/page-panel/index.ts create mode 100644 packages/tldraw/src/components/page-panel/page-panel.test.tsx create mode 100644 packages/tldraw/src/components/page-panel/page-panel.tsx create mode 100644 packages/tldraw/src/state/command/change-page/change-page.command.spec.ts create mode 100644 packages/tldraw/src/state/command/change-page/change-page.command.ts create mode 100644 packages/tldraw/src/state/command/change-page/index.ts create mode 100644 packages/tldraw/src/state/command/create-page/create-page.command.spec.ts create mode 100644 packages/tldraw/src/state/command/create-page/create-page.command.ts create mode 100644 packages/tldraw/src/state/command/create-page/index.ts create mode 100644 packages/tldraw/src/state/command/delete-page/delete-page.command.spec.ts create mode 100644 packages/tldraw/src/state/command/delete-page/delete-page.command.ts create mode 100644 packages/tldraw/src/state/command/delete-page/index.ts create mode 100644 packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.spec.ts create mode 100644 packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts create mode 100644 packages/tldraw/src/state/command/duplicate-page/index.ts create mode 100644 packages/tldraw/src/state/command/rename-page/index.ts create mode 100644 packages/tldraw/src/state/command/rename-page/rename-page.command.spec.ts create mode 100644 packages/tldraw/src/state/command/rename-page/rename-page.command.ts diff --git a/packages/core/src/components/binding/binding.test.tsx b/packages/core/src/components/binding/binding.test.tsx index a544ecdee..fc05a217b 100644 --- a/packages/core/src/components/binding/binding.test.tsx +++ b/packages/core/src/components/binding/binding.test.tsx @@ -5,7 +5,7 @@ import { Binding } from './binding' jest.spyOn(console, 'error').mockImplementation(() => void null) describe('binding', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithSvg() }) }) diff --git a/packages/core/src/components/bounds/bounds.test.tsx b/packages/core/src/components/bounds/bounds.test.tsx index c5da0bffb..8bcc03391 100644 --- a/packages/core/src/components/bounds/bounds.test.tsx +++ b/packages/core/src/components/bounds/bounds.test.tsx @@ -3,7 +3,7 @@ import { renderWithSvg } from '+test' import { Bounds } from './bounds' describe('bounds', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithSvg( { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithSvg() }) }) diff --git a/packages/core/src/components/canvas/canvas.test.tsx b/packages/core/src/components/canvas/canvas.test.tsx index 5c815c23e..836c9db1f 100644 --- a/packages/core/src/components/canvas/canvas.test.tsx +++ b/packages/core/src/components/canvas/canvas.test.tsx @@ -3,7 +3,7 @@ import { mockDocument, renderWithContext } from '+test' import { Canvas } from './canvas' describe('page', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext( { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithSvg() }) }) diff --git a/packages/core/src/components/error-fallback/error-fallback.test.tsx b/packages/core/src/components/error-fallback/error-fallback.test.tsx index d87edebc3..be41db4f4 100644 --- a/packages/core/src/components/error-fallback/error-fallback.test.tsx +++ b/packages/core/src/components/error-fallback/error-fallback.test.tsx @@ -3,7 +3,7 @@ import { renderWithContext } from '+test' import { ErrorFallback } from './error-fallback' describe('error fallback', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext( void null} />) }) }) diff --git a/packages/core/src/components/handles/handles.test.tsx b/packages/core/src/components/handles/handles.test.tsx index 3207a3823..5be665bf8 100644 --- a/packages/core/src/components/handles/handles.test.tsx +++ b/packages/core/src/components/handles/handles.test.tsx @@ -3,7 +3,7 @@ import { mockUtils, renderWithContext } from '+test' import { Handles } from './handles' describe('handles', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext() }) }) diff --git a/packages/core/src/components/page/page.test.tsx b/packages/core/src/components/page/page.test.tsx index 429099ef8..a380d5437 100644 --- a/packages/core/src/components/page/page.test.tsx +++ b/packages/core/src/components/page/page.test.tsx @@ -3,7 +3,7 @@ import { mockDocument, renderWithContext } from '+test' import { Page } from './page' describe('page', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext( { - test('mounts component', () => { + test('mounts component without crashing', () => { render( { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithSvg() }) }) diff --git a/packages/core/src/components/shape/shape.test.tsx b/packages/core/src/components/shape/shape.test.tsx index c599a0024..9e0b1af1f 100644 --- a/packages/core/src/components/shape/shape.test.tsx +++ b/packages/core/src/components/shape/shape.test.tsx @@ -3,7 +3,7 @@ import { mockUtils, renderWithSvg } from '+test' import { Shape } from './shape' describe('handles', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithSvg( { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext(
Hello
diff --git a/packages/tldraw/src/components/menu/index.ts b/packages/tldraw/src/components/menu/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/tldraw/src/components/menu/menu.test.tsx b/packages/tldraw/src/components/menu/menu.test.tsx new file mode 100644 index 000000000..d5aa15b70 --- /dev/null +++ b/packages/tldraw/src/components/menu/menu.test.tsx @@ -0,0 +1,9 @@ +import * as React from 'react' +import { Menu } from './menu' +import { mockDocument, renderWithContext } from '~test' + +describe('menu menu', () => { + test('mounts component without crashing', () => { + renderWithContext() + }) +}) diff --git a/packages/tldraw/src/components/menu/menu.tsx b/packages/tldraw/src/components/menu/menu.tsx new file mode 100644 index 000000000..22bf5c92b --- /dev/null +++ b/packages/tldraw/src/components/menu/menu.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { + FloatingContainer, + DropdownMenuRoot, + MenuContent, + IconButton, + breakpoints, + DropdownMenuButton, + DropdownMenuSubMenu, + DropdownMenuDivider, + DropdownMenuCheckboxItem, + IconWrapper, + Kbd, +} from '~components/shared' +import { useTLDrawContext, useTheme } from '~hooks' +import type { Data } from '~types' + +export const Menu = React.memo(() => { + const { tlstate } = useTLDrawContext() + + const handleNew = React.useCallback(() => { + tlstate.newProject() + }, [tlstate]) + + const handleSave = React.useCallback(() => { + tlstate.saveProject() + }, [tlstate]) + + const handleLoad = React.useCallback(() => { + tlstate.loadProject() + }, [tlstate]) + + const toggleDebugMode = React.useCallback(() => { + tlstate.toggleDebugMode() + }, [tlstate]) + + const handleSignOut = React.useCallback(() => { + tlstate.signOut() + }, [tlstate]) + + return ( + + + + + + + + New Project + #N + + + + Open... + #L + + + + + Save + #S + + + Save As... + ⇧#S + + + + + + Sign Out + + + + + + + + ) +}) + +function RecentFiles() { + return ( + + + Project A + + + Project B + + + Project C + + + ) +} + +const isDebugModeSelector = (s: Data) => s.settings.isDebugMode + +function Preferences() { + const { tlstate, useSelector } = useTLDrawContext() + const { theme, setTheme } = useTheme() + + const isDebugMode = useSelector(isDebugModeSelector) + const isDarkMode = theme === 'dark' + + const toggleDebugMode = React.useCallback(() => { + tlstate.toggleDebugMode() + }, [tlstate]) + + return ( + + setTheme(isDarkMode ? 'light' : 'dark')} + > + Dark Mode + + + Debug Mode + + + ) +} diff --git a/packages/tldraw/src/components/menu/preferences.tsx b/packages/tldraw/src/components/menu/preferences.tsx new file mode 100644 index 000000000..56a46682c --- /dev/null +++ b/packages/tldraw/src/components/menu/preferences.tsx @@ -0,0 +1,20 @@ +export function Preferences() { + const { theme, setTheme } = useTheme() + + const isDebugMode = useSelector((s) => s.data.settings.isDebugMode) + const isDarkMode = theme === 'dark' + + return ( + + setTheme(isDarkMode ? 'light' : 'dark')} + > + Dark Mode + + + Debug Mode + + + ) +} diff --git a/packages/tldraw/src/components/page-options-dialog/index.ts b/packages/tldraw/src/components/page-options-dialog/index.ts new file mode 100644 index 000000000..2d13bdb3b --- /dev/null +++ b/packages/tldraw/src/components/page-options-dialog/index.ts @@ -0,0 +1 @@ +export * from './page-options-dialog' diff --git a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx new file mode 100644 index 000000000..4f1b13ea1 --- /dev/null +++ b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.test.tsx @@ -0,0 +1,9 @@ +import * as React from 'react' +import { PageOptionsDialog } from './page-options-dialog' +import { mockDocument, renderWithContext } from '~test' + +describe('page options dialog', () => { + test('mounts component without crashing', () => { + renderWithContext() + }) +}) diff --git a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx new file mode 100644 index 000000000..c9121d65b --- /dev/null +++ b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx @@ -0,0 +1,122 @@ +import * as React from 'react' +import * as Dialog from '@radix-ui/react-alert-dialog' +import { MixerVerticalIcon } from '@radix-ui/react-icons' +import { + breakpoints, + IconButton, + DialogOverlay, + DialogContent, + RowButton, + MenuTextInput, + DialogInputWrapper, + Divider, +} from '~components/shared' +import type { Data, TLDrawPage } from '~types' +import { useTLDrawContext } from '~hooks' + +const canDeleteSelector = (s: Data) => { + // TODO: Include all pages + return [s.page].length <= 1 +} + +export function PageOptionsDialog({ page }: { page: TLDrawPage }): JSX.Element { + const { tlstate, useSelector } = useTLDrawContext() + + const [isOpen, setIsOpen] = React.useState(false) + + const canDelete = useSelector(canDeleteSelector) + + const rInput = React.useRef(null) + + const [name, setName] = React.useState(page.name || 'Page') + + const handleNameChange = React.useCallback((e: React.ChangeEvent) => { + setName(e.currentTarget.value) + }, []) + + const handleDuplicate = React.useCallback(() => { + tlstate.duplicatePage(page.id) + }, [tlstate]) + + const handleDelete = React.useCallback(() => { + tlstate.deletePage(page.id) + }, [tlstate]) + + const handleOpenChange = React.useCallback( + (isOpen: boolean) => { + setIsOpen(isOpen) + + if (isOpen) { + return + } + + if (name.length === 0) { + tlstate.renamePage(page.id, 'Page') + } + }, + [tlstate, name] + ) + + const handleSave = React.useCallback(() => { + tlstate.renamePage(page.id, name) + }, [tlstate, name]) + + function stopPropagation(e: React.KeyboardEvent) { + e.stopPropagation() + } + + function handleKeydown(e: React.KeyboardEvent) { + if (e.key === 'Enter') { + handleSave() + setIsOpen(false) + } + } + + React.useEffect(() => { + if (isOpen) { + setTimeout(() => { + rInput.current?.focus() + rInput.current?.select() + }, 0) + } + }, [isOpen]) + + return ( + + + + + + + + + + + + Duplicate + + + Delete + + + + Save + + + Cancel + + + + ) +} diff --git a/packages/tldraw/src/components/page-panel/index.ts b/packages/tldraw/src/components/page-panel/index.ts new file mode 100644 index 000000000..333268ef3 --- /dev/null +++ b/packages/tldraw/src/components/page-panel/index.ts @@ -0,0 +1 @@ +export * from './page-panel' diff --git a/packages/tldraw/src/components/page-panel/page-panel.test.tsx b/packages/tldraw/src/components/page-panel/page-panel.test.tsx new file mode 100644 index 000000000..bb577e494 --- /dev/null +++ b/packages/tldraw/src/components/page-panel/page-panel.test.tsx @@ -0,0 +1,9 @@ +import * as React from 'react' +import { PagePanel } from './page-panel' +import { renderWithContext } from '~test' + +describe('page panel', () => { + test('mounts component without crashing', () => { + renderWithContext() + }) +}) diff --git a/packages/tldraw/src/components/page-panel/page-panel.tsx b/packages/tldraw/src/components/page-panel/page-panel.tsx new file mode 100644 index 000000000..0752c64ed --- /dev/null +++ b/packages/tldraw/src/components/page-panel/page-panel.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { PlusIcon, CheckIcon } from '@radix-ui/react-icons' +import { + breakpoints, + DropdownMenuButton, + DropdownMenuDivider, + RowButton, + MenuContent, + FloatingContainer, + IconWrapper, +} from '~components/shared' +import { PageOptionsDialog } from '~components/page-options-dialog' +import styled from '~styles' +import { useTLDrawContext } from '~hooks' +import type { Data } from '~types' + +const currentPageSelector = (s: Data) => s.page + +export function PagePanel(): JSX.Element { + const rIsOpen = React.useRef(false) + const [isOpen, setIsOpen] = React.useState(false) + + const { tlstate, useSelector } = useTLDrawContext() + + React.useEffect(() => { + if (rIsOpen.current !== isOpen) { + rIsOpen.current = isOpen + } + }, [isOpen]) + + const handleCreatePage = React.useCallback(() => { + tlstate.createPage() + }, [tlstate]) + + const handleChangePage = React.useCallback( + (id: string) => { + setIsOpen(false) + tlstate.changePage(id) + }, + [tlstate] + ) + + const currentPage = useSelector(currentPageSelector) + + const sorted = Object.values([currentPage]).sort( + (a, b) => (a.childIndex || 0) - (b.childIndex || 0) + ) + + return ( + { + if (rIsOpen.current !== isOpen) { + setIsOpen(isOpen) + } + }} + > + + + {currentPage.name || 'Page'} + + + + + {sorted.map((page) => ( + + + {page.name} + + + + + + + + + ))} + + + + Create Page + + + + + + + ) +} + +const ButtonWithOptions = styled('div', { + display: 'grid', + gridTemplateColumns: '1fr auto', + gridAutoFlow: 'column', + + '& > *[data-shy="true"]': { + opacity: 0, + }, + + '&:hover > *[data-shy="true"]': { + opacity: 1, + }, +}) diff --git a/packages/tldraw/src/components/style-panel/style-panel.test.tsx b/packages/tldraw/src/components/style-panel/style-panel.test.tsx index cbded188b..985c597b6 100644 --- a/packages/tldraw/src/components/style-panel/style-panel.test.tsx +++ b/packages/tldraw/src/components/style-panel/style-panel.test.tsx @@ -3,7 +3,7 @@ import { renderWithContext } from '~test' import { StylePanel } from './style-panel' describe('style panel', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext() }) }) diff --git a/packages/tldraw/src/components/tldraw/tldraw.test.tsx b/packages/tldraw/src/components/tldraw/tldraw.test.tsx index 7c5c5c6ae..9b67d61b2 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.test.tsx +++ b/packages/tldraw/src/components/tldraw/tldraw.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react' import { TLDraw } from './tldraw' describe('tldraw', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { render() }) }) diff --git a/packages/tldraw/src/components/tldraw/tldraw.tsx b/packages/tldraw/src/components/tldraw/tldraw.tsx index b1c005211..80cadaec9 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.tsx +++ b/packages/tldraw/src/components/tldraw/tldraw.tsx @@ -124,6 +124,7 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange } onTextKeyUp={tlstate.onTextKeyUp} /> + @@ -137,10 +138,10 @@ const Spacer = styled('div', { flexGrow: 2, }) -// const MenuButtons = styled('div', { -// display: 'flex', -// gap: 8, -// }) +const MenuButtons = styled('div', { + display: 'flex', + gap: 8, +}) const Layout = styled('main', { position: 'fixed', diff --git a/packages/tldraw/src/components/tools-panel/tools-panel.test.tsx b/packages/tldraw/src/components/tools-panel/tools-panel.test.tsx index b9cbd157b..12ba8d9f6 100644 --- a/packages/tldraw/src/components/tools-panel/tools-panel.test.tsx +++ b/packages/tldraw/src/components/tools-panel/tools-panel.test.tsx @@ -3,7 +3,7 @@ import { ToolsPanel } from './tools-panel' import { renderWithContext } from '~test' describe('tools panel', () => { - test('mounts component', () => { + test('mounts component without crashing', () => { renderWithContext() }) }) diff --git a/packages/tldraw/src/hooks/useTheme.ts b/packages/tldraw/src/hooks/useTheme.ts index abc978c11..9667d184d 100644 --- a/packages/tldraw/src/hooks/useTheme.ts +++ b/packages/tldraw/src/hooks/useTheme.ts @@ -4,5 +4,6 @@ export function useTheme() { return { theme: 'light' as Theme, toggle: () => null, + setTheme: (theme: Theme) => void theme, } } diff --git a/packages/tldraw/src/state/command/change-page/change-page.command.spec.ts b/packages/tldraw/src/state/command/change-page/change-page.command.spec.ts new file mode 100644 index 000000000..70a52339b --- /dev/null +++ b/packages/tldraw/src/state/command/change-page/change-page.command.spec.ts @@ -0,0 +1,28 @@ +import { TLDrawState } from '~state' +import { mockDocument } from '~test' + +describe('Change page command', () => { + const tlstate = new TLDrawState() + + it('does, undoes and redoes command', () => { + tlstate.loadDocument(mockDocument) + + const initialId = tlstate.page.id + + tlstate.createPage() + + const nextId = tlstate.page.id + + tlstate.changePage(initialId) + + expect(tlstate.page.id).toBe(initialId) + + tlstate.undo() + + expect(tlstate.page.id).toBe(nextId) + + tlstate.redo() + + expect(tlstate.page.id).toBe(initialId) + }) +}) diff --git a/packages/tldraw/src/state/command/change-page/change-page.command.ts b/packages/tldraw/src/state/command/change-page/change-page.command.ts new file mode 100644 index 000000000..05c9f040a --- /dev/null +++ b/packages/tldraw/src/state/command/change-page/change-page.command.ts @@ -0,0 +1,10 @@ +import type { TLDrawShape, Data, Command } from '~types' +import { TLDR } from '~state/tldr' + +export function changePage(data: Data): Command { + return { + id: 'create_page', + before: {}, + after: {}, + } +} diff --git a/packages/tldraw/src/state/command/change-page/index.ts b/packages/tldraw/src/state/command/change-page/index.ts new file mode 100644 index 000000000..9bb65fb23 --- /dev/null +++ b/packages/tldraw/src/state/command/change-page/index.ts @@ -0,0 +1 @@ +export * from './change-page.command' diff --git a/packages/tldraw/src/state/command/create-page/create-page.command.spec.ts b/packages/tldraw/src/state/command/create-page/create-page.command.spec.ts new file mode 100644 index 000000000..f3c79ffe5 --- /dev/null +++ b/packages/tldraw/src/state/command/create-page/create-page.command.spec.ts @@ -0,0 +1,26 @@ +import { TLDrawState } from '~state' +import { mockDocument } from '~test' + +describe('Create page command', () => { + const tlstate = new TLDrawState() + + it('does, undoes and redoes command', () => { + tlstate.loadDocument(mockDocument) + + const initialId = tlstate.page.id + + tlstate.createPage() + + const nextId = tlstate.page.id + + expect(tlstate.page.id).toBe(nextId) + + tlstate.undo() + + expect(tlstate.page.id).toBe(initialId) + + tlstate.redo() + + expect(tlstate.page.id).toBe(nextId) + }) +}) diff --git a/packages/tldraw/src/state/command/create-page/create-page.command.ts b/packages/tldraw/src/state/command/create-page/create-page.command.ts new file mode 100644 index 000000000..d3fc6025b --- /dev/null +++ b/packages/tldraw/src/state/command/create-page/create-page.command.ts @@ -0,0 +1,10 @@ +import type { TLDrawShape, Data, Command } from '~types' +import { TLDR } from '~state/tldr' + +export function createPage(data: Data): Command { + return { + id: 'create_page', + before: {}, + after: {}, + } +} diff --git a/packages/tldraw/src/state/command/create-page/index.ts b/packages/tldraw/src/state/command/create-page/index.ts new file mode 100644 index 000000000..3a7e688ee --- /dev/null +++ b/packages/tldraw/src/state/command/create-page/index.ts @@ -0,0 +1 @@ +export * from './create-page.command' diff --git a/packages/tldraw/src/state/command/delete-page/delete-page.command.spec.ts b/packages/tldraw/src/state/command/delete-page/delete-page.command.spec.ts new file mode 100644 index 000000000..41f779969 --- /dev/null +++ b/packages/tldraw/src/state/command/delete-page/delete-page.command.spec.ts @@ -0,0 +1,28 @@ +import { TLDrawState } from '~state' +import { mockDocument } from '~test' + +describe('Delete page', () => { + const tlstate = new TLDrawState() + + it('does, undoes and redoes command', () => { + tlstate.loadDocument(mockDocument) + + const initialId = tlstate.page.id + + tlstate.createPage() + + const nextId = tlstate.page.id + + tlstate.deletePage() + + expect(tlstate.page.id).toBe(nextId) + + tlstate.undo() + + expect(tlstate.page.id).toBe(initialId) + + tlstate.redo() + + expect(tlstate.page.id).toBe(nextId) + }) +}) diff --git a/packages/tldraw/src/state/command/delete-page/delete-page.command.ts b/packages/tldraw/src/state/command/delete-page/delete-page.command.ts new file mode 100644 index 000000000..99f58d545 --- /dev/null +++ b/packages/tldraw/src/state/command/delete-page/delete-page.command.ts @@ -0,0 +1,10 @@ +import type { TLDrawShape, Data, Command } from '~types' +import { TLDR } from '~state/tldr' + +export function deletePage(data: Data, id: string): Command { + return { + id: 'delete_page', + before: {}, + after: {}, + } +} diff --git a/packages/tldraw/src/state/command/delete-page/index.ts b/packages/tldraw/src/state/command/delete-page/index.ts new file mode 100644 index 000000000..e2cf32f75 --- /dev/null +++ b/packages/tldraw/src/state/command/delete-page/index.ts @@ -0,0 +1 @@ +export * from './delete-page.command' diff --git a/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.spec.ts b/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.spec.ts new file mode 100644 index 000000000..97de3ffb2 --- /dev/null +++ b/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.spec.ts @@ -0,0 +1,24 @@ +import { TLDrawState } from '~state' +import { mockDocument } from '~test' + +describe('Duplicate page', () => { + const tlstate = new TLDrawState() + + it('does, undoes and redoes command', () => { + tlstate.loadDocument(mockDocument) + + const initialId = tlstate.page.id + + tlstate.duplicatePage() + + const nextId = tlstate.page.id + + tlstate.undo() + + expect(tlstate.page.id).toBe(initialId) + + tlstate.redo() + + expect(tlstate.page.id).toBe(nextId) + }) +}) diff --git a/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts b/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts new file mode 100644 index 000000000..f54aa0c33 --- /dev/null +++ b/packages/tldraw/src/state/command/duplicate-page/duplicate-page.command.ts @@ -0,0 +1,10 @@ +import type { TLDrawShape, Data, Command } from '~types' +import { TLDR } from '~state/tldr' + +export function duplicatePage(data: Data, id: string): Command { + return { + id: 'duplicate_page', + before: {}, + after: {}, + } +} diff --git a/packages/tldraw/src/state/command/duplicate-page/index.ts b/packages/tldraw/src/state/command/duplicate-page/index.ts new file mode 100644 index 000000000..88b563600 --- /dev/null +++ b/packages/tldraw/src/state/command/duplicate-page/index.ts @@ -0,0 +1 @@ +export * from './duplicate-page.command' diff --git a/packages/tldraw/src/state/command/rename-page/index.ts b/packages/tldraw/src/state/command/rename-page/index.ts new file mode 100644 index 000000000..950267b07 --- /dev/null +++ b/packages/tldraw/src/state/command/rename-page/index.ts @@ -0,0 +1 @@ +export * from './rename-page.command' diff --git a/packages/tldraw/src/state/command/rename-page/rename-page.command.spec.ts b/packages/tldraw/src/state/command/rename-page/rename-page.command.spec.ts new file mode 100644 index 000000000..98c740152 --- /dev/null +++ b/packages/tldraw/src/state/command/rename-page/rename-page.command.spec.ts @@ -0,0 +1,25 @@ +import { TLDrawState } from '~state' +import { mockDocument } from '~test' + +describe('Edit page', () => { + const tlstate = new TLDrawState() + + it('does, undoes and redoes command', () => { + tlstate.loadDocument(mockDocument) + + const initialId = tlstate.page.id + const initialName = tlstate.page.name + + tlstate.renamePage(initialId, 'My Special Page') + + expect(tlstate.page.name).toBe('My Special Page') + + tlstate.undo() + + expect(tlstate.page.name).toBe(initialName) + + tlstate.redo() + + expect(tlstate.page.name).toBe('My Special Page') + }) +}) diff --git a/packages/tldraw/src/state/command/rename-page/rename-page.command.ts b/packages/tldraw/src/state/command/rename-page/rename-page.command.ts new file mode 100644 index 000000000..1d9493521 --- /dev/null +++ b/packages/tldraw/src/state/command/rename-page/rename-page.command.ts @@ -0,0 +1,10 @@ +import type { TLDrawShape, Data, Command } from '~types' +import { TLDR } from '~state/tldr' + +export function editPage(data: Data, id: string): Command { + return { + id: 'edit_page', + before: {}, + after: {}, + } +} diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 54780051a..ce7fb2704 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -519,7 +519,22 @@ export class TLDrawState implements TLCallbacks { return this } - /* ---------------------- Document --------------------- */ + /* -------------------------------------------------- */ + /* Document */ + /* -------------------------------------------------- */ + + private setCurrentPageId(pageId: string) { + if (pageId === this.currentPageId) return this + + this.currentPageId = pageId + + this.setState({ + page: this.pages[pageId], + pageState: this.pageStates[pageId], + }) + return this + } + loadDocument = (document: TLDrawDocument, onChange?: TLDrawState['_onChange']) => { this._onChange = onChange this.currentDocumentId = document.id @@ -543,19 +558,26 @@ export class TLDrawState implements TLCallbacks { return this } - setCurrentPageId(pageId: string) { - if (pageId === this.currentPageId) return this - - this.currentPageId = pageId - - this.setState({ - page: this.pages[pageId], - pageState: this.pageStates[pageId], - }) - return this + newProject = () => { + // TODO } - /* -------------------- Sessions -------------------- */ + saveProject = () => { + // TODO + } + + loadProject = () => { + // TODO + } + + signOut = () => { + // TODO + } + + /* -------------------------------------------------- */ + /* Sessions */ + /* -------------------------------------------------- */ + startSession(session: T, ...args: ParametersExceptFirst) { this.session = session this.setState((data) => session.start(data, ...args), session.status) @@ -658,7 +680,10 @@ export class TLDrawState implements TLCallbacks { return this } - /* -------------------- Commands -------------------- */ + /* -------------------------------------------------- */ + /* History */ + /* -------------------------------------------------- */ + do(command: Command) { const { history } = this @@ -739,7 +764,10 @@ export class TLDrawState implements TLCallbacks { return this } - /* -------------------- Selection ------------------- */ + /* -------------------------------------------------- */ + /* Selection */ + /* -------------------------------------------------- */ + setSelectedIds(ids: string[], push = false) { this.setState((data) => { return { @@ -936,6 +964,10 @@ export class TLDrawState implements TLCallbacks { return this } + toggleDebugMode = () => { + // TODO + } + rotate = (delta = Math.PI * -0.5, ids?: string[]) => { const data = this.store.getState() const idsToMutate = ids ? ids : data.pageState.selectedIds @@ -1006,6 +1038,52 @@ export class TLDrawState implements TLCallbacks { return this } + createPage() { + const newId = Utils.uniqueId() + this.pages[newId] = { id: newId, shapes: {}, bindings: {} } + this.changePage(newId) + return this + } + + changePage(id: string) { + this.setCurrentPageId(id) + return this + } + + renamePage(id: string, name: string) { + this.pages[id] = { ...this.pages[id], name } + return this + } + + duplicatePage(id: string = this.currentPageId) { + const newId = Utils.uniqueId() + this.pages[newId] = { ...this.pages[id], id: newId } + this.changePage(newId) + return this + } + + deletePage(id: string = this.currentPageId) { + const pages = Object.values(this.pages).sort( + (a, b) => (a.childIndex || 0) - (b.childIndex || 0) + ) + + const currentIndex = pages.findIndex((page) => page.id === this.currentPageId) + + if (Object.values(this.pages).length <= 1) return + + delete this.pages[id] + + if (id === this.currentPageId) { + if (currentIndex === pages.length - 1) { + this.changePage(pages[pages.length - 2].id) + } else { + this.changePage(pages[currentIndex + 1].id) + } + } + + return this + } + copy = (ids?: string[]) => { const data = this.store.getState() const idsToCopy = ids ? ids : data.pageState.selectedIds diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index e75ef10d1..fd1307e25 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -8,9 +8,11 @@ import type { StoreApi } from 'zustand' export type TLStore = StoreApi export type TLChange = Data +export type TLDrawPage = TLPage + export interface TLDrawDocument { id: string - pages: Record> + pages: Record pageStates: Record } diff --git a/yarn.lock b/yarn.lock index 8cebeb05e..397c2dd95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1634,6 +1634,20 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-alert-dialog@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.0.20.tgz#1adb997c899fd9cb6f0d13bc66b50d2166414339" + integrity sha512-Vaz16wc4rDVHC7BH2At3TM+HEU56jN6fAWoeXatZyv1BAaehSabusbghC4V8Cfc0llP4ijvOY7Eznkt0+jP/fQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dialog" "0.0.20" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-slot" "0.0.12" + "@radix-ui/react-arrow@0.0.14": version "0.0.14" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-0.0.14.tgz#70b2c66efbf3cde0c9dd0895417e39f6cdf31805" @@ -1696,6 +1710,28 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.20.tgz#b26607bea68fc20067d06fab996bac7f1acf68c1" + integrity sha512-fXgWxWyvmNiimxrFGdvUNve0tyQEFyPwrNgkSi6Xiha9cX8sqWdiYWq500zhzUQQFJVS7No73ylx8kgrI7SoLw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-context" "0.0.5" + "@radix-ui/react-dismissable-layer" "0.0.15" + "@radix-ui/react-focus-guards" "0.0.7" + "@radix-ui/react-focus-scope" "0.0.15" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-portal" "0.0.15" + "@radix-ui/react-presence" "0.0.15" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-slot" "0.0.12" + "@radix-ui/react-use-controllable-state" "0.0.6" + aria-hidden "^1.1.1" + react-remove-scroll "^2.4.0" + "@radix-ui/react-dismissable-layer@0.0.14": version "0.0.14" resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.14.tgz#9d8a3415a2830688070c6596dec18b43c33df7b2" @@ -1709,6 +1745,19 @@ "@radix-ui/react-use-callback-ref" "0.0.5" "@radix-ui/react-use-escape-keydown" "0.0.6" +"@radix-ui/react-dismissable-layer@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.15.tgz#02c0e68684d60933c82b5af6793c87a5f9ee0750" + integrity sha512-2zABi8rh/t6liFfRLBw6h+B7MNNFxVQrgYfWRMs1elNX41z3G2vLoBlWdqGzAlYrtqEr/6CL4pQfhwVtd7rNGw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-body-pointer-events" "0.0.7" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-escape-keydown" "0.0.6" + "@radix-ui/react-dropdown-menu@^0.0.22": version "0.0.22" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.0.22.tgz#914dbbb12d31b4379df697f1262150b3f50916ae" @@ -1742,6 +1791,17 @@ "@radix-ui/react-primitive" "0.0.14" "@radix-ui/react-use-callback-ref" "0.0.5" +"@radix-ui/react-focus-scope@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.0.15.tgz#60917075e53ee72d2a473fba88eb31e7aaf7d841" + integrity sha512-zNgEe1lyLPfxa003VD8lCXaadGqCYhboA3X1WDNGes74lzJgLOPJgzLI0F/ksSokkx/yDDdReyOWui3/LCTqTw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-icons@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.0.3.tgz#4ef61f1234f44991f7a19e108f77ca37032b4be2" @@ -1796,6 +1856,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-polymorphic/-/react-polymorphic-0.0.12.tgz#bf4ae516669b68e059549538104d97322f7c876b" integrity sha512-/GYNMicBnGzjD1d2fCAuzql1VeFrp8mqM3xfzT1kxhnV85TKdURO45jBfMgqo17XNXoNhWIAProUsCO4qFAAIg== +"@radix-ui/react-polymorphic@0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@radix-ui/react-polymorphic/-/react-polymorphic-0.0.13.tgz#d010d48281626191c9513f11db5d82b37662418a" + integrity sha512-0sGqBp+v9/yrsMhPfAejxcem2MwAFgaSAxF3Sieaklm6ZVYM/hTZxxWI5NVOLGV+482GwW0wIqwpVUzREjmh+w== + "@radix-ui/react-popper@0.0.17": version "0.0.17" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.0.17.tgz#a73486e19a628cb3fecaf3fb6eecf6e2cab9d0be" @@ -1822,6 +1887,16 @@ "@radix-ui/react-primitive" "0.0.14" "@radix-ui/react-use-layout-effect" "0.0.5" +"@radix-ui/react-portal@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.0.15.tgz#833bccb192aafb9420bd037d5827e88caf429dc4" + integrity sha512-qMESsdqph1gbRGzy9oSzUoeZYXnR2egXVcEZDqmesfn8w/o1rC1wadKkyBf7qo/YyjUX4mvXknAA+ftp1aQp+w== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-primitive" "0.0.15" + "@radix-ui/react-use-layout-effect" "0.0.5" + "@radix-ui/react-presence@0.0.14": version "0.0.14" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.0.14.tgz#6a86058bbbf46234dd8840dacd620b3ac5797025" @@ -1830,6 +1905,15 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "0.0.5" +"@radix-ui/react-presence@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.0.15.tgz#4ff12feb436f1499148feb11c3a63a5d8fab568a" + integrity sha512-+5+ePKUdTkqN1ze7nYmcoeHSsmKCcREwt0NhvNgDocPaqEUoZSkK9Mq6eMiMXSj02NkXH9P+bK32VCClYFnMBQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-use-layout-effect" "0.0.5" + "@radix-ui/react-primitive@0.0.14": version "0.0.14" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.0.14.tgz#752a967cb05d4c5643634fe20274e7dc905d1cce" @@ -1838,6 +1922,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-polymorphic" "0.0.12" +"@radix-ui/react-primitive@0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.0.15.tgz#c0cf609ee565a32969d20943e2697b42a04fbdf3" + integrity sha512-Y7JLnen/G3AT0cQXXkBo3A1OuWaKGerkd2gKs0Fuqxv+kTxEmYoqSp/soo0Mm3Ccw61LKLQAjPiE37GK9/Zqwg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-polymorphic" "0.0.13" + "@radix-ui/react-radio-group@^0.0.18": version "0.0.18" resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.0.18.tgz#d6f9ce132102deb23ee782e08f7b3e185ea317f0" @@ -1912,6 +2004,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "0.0.5" +"@radix-ui/react-use-body-pointer-events@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.7.tgz#e4249690ca0db85c969400e867476206feda4d1e" + integrity sha512-mXAGyb8mhVjRqtpKPeZePuvee40bgsWpt378oQrIcLU1uZNbNX9eyrIPnnL9OMLAvxqloAOClVj0PZ1bMQmfDw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "0.0.5" + "@radix-ui/react-use-callback-ref@0.0.5": version "0.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.0.5.tgz#fa8db050229cda573dfeeae213d74ef06f6130db"