Adds create / delete pages
This commit is contained in:
parent
3b74580b4f
commit
507c081bd0
9 changed files with 301 additions and 28 deletions
|
@ -1,15 +1,28 @@
|
|||
import styled from 'styles'
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
|
||||
import { IconWrapper, RowButton } from 'components/shared'
|
||||
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons'
|
||||
import { CheckIcon, ChevronDownIcon, PlusIcon } from '@radix-ui/react-icons'
|
||||
import * as Panel from '../panel'
|
||||
import state, { useSelector } from 'state'
|
||||
import { getPage } from 'utils/utils'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export default function PagePanel() {
|
||||
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||
const rIsOpen = useRef(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (rIsOpen.current !== isOpen) {
|
||||
rIsOpen.current = isOpen
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const documentPages = useSelector((s) => s.data.document.pages)
|
||||
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||
|
||||
if (!documentPages[currentPageId]) return null
|
||||
|
||||
const sorted = Object.values(documentPages).sort(
|
||||
(a, b) => a.childIndex - b.childIndex
|
||||
|
@ -17,7 +30,14 @@ export default function PagePanel() {
|
|||
|
||||
return (
|
||||
<OuterContainer>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Root
|
||||
open={isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (rIsOpen.current !== isOpen) {
|
||||
setIsOpen(isOpen)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PanelRoot>
|
||||
<DropdownMenu.Trigger as={RowButton}>
|
||||
<span>{documentPages[currentPageId].name}</span>
|
||||
|
@ -30,19 +50,56 @@ export default function PagePanel() {
|
|||
<DropdownMenu.RadioGroup
|
||||
as={Content}
|
||||
value={currentPageId}
|
||||
onValueChange={(id) =>
|
||||
onValueChange={(id) => {
|
||||
setIsOpen(false)
|
||||
state.send('CHANGED_CURRENT_PAGE', { id })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{sorted.map(({ id, name }) => (
|
||||
<StyledRadioItem key={id} value={id}>
|
||||
<span>{name}</span>
|
||||
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</StyledRadioItem>
|
||||
<ContextMenu.Root key={id}>
|
||||
<ContextMenu.Trigger>
|
||||
<StyledRadioItem key={id} value={id}>
|
||||
<span>{name}</span>
|
||||
<DropdownMenu.ItemIndicator
|
||||
as={IconWrapper}
|
||||
size="small"
|
||||
>
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</StyledRadioItem>
|
||||
</ContextMenu.Trigger>
|
||||
<StyledContextMenuContent>
|
||||
<ContextMenu.Group>
|
||||
<StyledContextMenuItem
|
||||
onSelect={() => state.send('RENAMED_PAGE', { id })}
|
||||
>
|
||||
Rename
|
||||
</StyledContextMenuItem>
|
||||
<StyledContextMenuItem
|
||||
onSelect={() => {
|
||||
setIsOpen(false)
|
||||
state.send('DELETED_PAGE', { id })
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</StyledContextMenuItem>
|
||||
</ContextMenu.Group>
|
||||
</StyledContextMenuContent>
|
||||
</ContextMenu.Root>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
<DropdownMenu.Separator />
|
||||
<RowButton
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
state.send('CREATED_PAGE')
|
||||
}}
|
||||
>
|
||||
<span>Create Page</span>
|
||||
<IconWrapper size="small">
|
||||
<PlusIcon />
|
||||
</IconWrapper>
|
||||
</RowButton>
|
||||
</PanelRoot>
|
||||
</DropdownMenu.Content>
|
||||
</PanelRoot>
|
||||
|
@ -58,6 +115,7 @@ const PanelRoot = styled('div', {
|
|||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: '2px',
|
||||
|
@ -65,6 +123,7 @@ const PanelRoot = styled('div', {
|
|||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
userSelect: 'none',
|
||||
})
|
||||
|
||||
const Content = styled(Panel.Content, {
|
||||
|
@ -87,6 +146,9 @@ const StyledRadioItem = styled(DropdownMenu.RadioItem, {
|
|||
'&:hover': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
'&:focus-within': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
})
|
||||
|
||||
const OuterContainer = styled('div', {
|
||||
|
@ -100,3 +162,56 @@ const OuterContainer = styled('div', {
|
|||
zIndex: 200,
|
||||
height: 44,
|
||||
})
|
||||
|
||||
const StyledContextMenuContent = styled(ContextMenu.Content, {
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
})
|
||||
|
||||
const StyledContextMenuItem = styled(ContextMenu.Item, {
|
||||
height: 32,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 12px 0 12px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
fontSize: '$1',
|
||||
fontFamily: '$ui',
|
||||
backgroundColor: 'transparent',
|
||||
outline: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledOverlay = styled(Dialog.Overlay, {
|
||||
backgroundColor: 'rgba(0, 0, 0, .15)',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
})
|
||||
|
||||
const StyledContent = styled(Dialog.Content, {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
minWidth: 200,
|
||||
maxWidth: 'fit-content',
|
||||
maxHeight: '85vh',
|
||||
padding: 20,
|
||||
marginTop: '-5vh',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 6,
|
||||
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -78,21 +78,10 @@ export const RowButton = styled('button', {
|
|||
fontSize: '$1',
|
||||
justifyContent: 'space-between',
|
||||
padding: '4px 6px 4px 12px',
|
||||
borderRadius: 4,
|
||||
|
||||
'&::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
zIndex: -1,
|
||||
},
|
||||
|
||||
'&:hover::before': {
|
||||
'&:hover': {
|
||||
backgroundColor: '$hover',
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
'& label': {
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.1.3",
|
||||
"@radix-ui/react-checkbox": "^0.0.15",
|
||||
"@radix-ui/react-context-menu": "^0.0.19",
|
||||
"@radix-ui/react-dialog": "^0.0.17",
|
||||
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
||||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^0.0.16",
|
||||
|
|
|
@ -5,7 +5,7 @@ import { getPage, getSelectedShapes } from 'utils/utils'
|
|||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import * as vec from 'utils/vec'
|
||||
|
||||
export default function nudgeCommand(data: Data, pageId: string) {
|
||||
export default function changePage(data: Data, pageId: string) {
|
||||
const { currentPageId: prevPageId } = data
|
||||
|
||||
history.execute(
|
||||
|
|
59
state/commands/create-page.ts
Normal file
59
state/commands/create-page.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, Page } from 'types'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { current } from 'immer'
|
||||
|
||||
export default function createPage(data: Data) {
|
||||
const snapshot = getSnapshot(data)
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'change_page',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
data.selectedIds.clear()
|
||||
const { page, pageState } = snapshot
|
||||
data.document.pages[page.id] = page
|
||||
data.pageStates[page.id] = pageState
|
||||
data.currentPageId = page.id
|
||||
},
|
||||
undo(data) {
|
||||
data.selectedIds.clear()
|
||||
const { page, currentPageId } = snapshot
|
||||
delete data.document.pages[page.id]
|
||||
delete data.pageStates[page.id]
|
||||
data.currentPageId = currentPageId
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function getSnapshot(data: Data) {
|
||||
const { currentPageId } = current(data)
|
||||
|
||||
const pages = Object.values(data.document.pages)
|
||||
const unchanged = pages.filter((page) => page.name.startsWith('Page '))
|
||||
const id = uuid()
|
||||
|
||||
const page: Page = {
|
||||
type: 'page',
|
||||
id,
|
||||
name: `Page ${unchanged.length + 1}`,
|
||||
childIndex: pages.length,
|
||||
shapes: {},
|
||||
}
|
||||
const pageState = {
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
currentPageId,
|
||||
page,
|
||||
pageState,
|
||||
}
|
||||
}
|
59
state/commands/delete-page.ts
Normal file
59
state/commands/delete-page.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import { current } from 'immer'
|
||||
import { getPage, getSelectedShapes } from 'utils/utils'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import * as vec from 'utils/vec'
|
||||
|
||||
export default function changePage(data: Data, pageId: string) {
|
||||
const snapshot = getSnapshot(data, pageId)
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
name: 'change_page',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
data.currentPageId = snapshot.nextPageId
|
||||
delete data.document.pages[pageId]
|
||||
delete data.pageStates[pageId]
|
||||
},
|
||||
undo(data) {
|
||||
data.currentPageId = snapshot.currentPageId
|
||||
data.document.pages[pageId] = snapshot.page
|
||||
data.pageStates[pageId] = snapshot.pageState
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function getSnapshot(data: Data, pageId: string) {
|
||||
const cData = current(data)
|
||||
const { currentPageId, document } = cData
|
||||
|
||||
const page = document.pages[pageId]
|
||||
const pageState = cData.pageStates[pageId]
|
||||
|
||||
const isCurrent = currentPageId === pageId
|
||||
|
||||
const nextIndex = isCurrent
|
||||
? page.childIndex === 0
|
||||
? 1
|
||||
: page.childIndex - 1
|
||||
: document.pages[currentPageId].childIndex
|
||||
|
||||
const nextPageId = isCurrent
|
||||
? Object.values(document.pages).find(
|
||||
(page) => page.childIndex === nextIndex
|
||||
)!.id
|
||||
: cData.currentPageId
|
||||
|
||||
return {
|
||||
nextPageId,
|
||||
isCurrent,
|
||||
currentPageId,
|
||||
page,
|
||||
pageState,
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import align from './align'
|
||||
import arrow from './arrow'
|
||||
import changePage from './change-page'
|
||||
import createPage from './create-page'
|
||||
import deleteSelected from './delete-selected'
|
||||
import deletePage from './delete-page'
|
||||
import direct from './direct'
|
||||
import distribute from './distribute'
|
||||
import draw from './draw'
|
||||
|
@ -23,6 +25,8 @@ const commands = {
|
|||
align,
|
||||
arrow,
|
||||
changePage,
|
||||
createPage,
|
||||
deletePage,
|
||||
deleteSelected,
|
||||
direct,
|
||||
distribute,
|
||||
|
|
|
@ -151,6 +151,8 @@ const state = createState({
|
|||
DISABLED_PEN_LOCK: 'disablePenLock',
|
||||
CLEARED_PAGE: ['selectAll', 'deleteSelection'],
|
||||
CHANGED_CURRENT_PAGE: ['clearSelectedIds', 'setCurrentPage'],
|
||||
CREATED_PAGE: ['clearSelectedIds', 'createPage'],
|
||||
DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
|
||||
},
|
||||
initial: 'selecting',
|
||||
states: {
|
||||
|
@ -742,12 +744,21 @@ const state = createState({
|
|||
isPenLocked(data) {
|
||||
return data.settings.isPenLocked
|
||||
},
|
||||
hasOnlyOnePage(data) {
|
||||
return Object.keys(data.document.pages).length === 1
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/* ---------------------- Pages --------------------- */
|
||||
setCurrentPage(data, payload: { id: string }) {
|
||||
commands.changePage(data, payload.id)
|
||||
},
|
||||
createPage(data) {
|
||||
commands.createPage(data)
|
||||
},
|
||||
deletePage(data, payload: { id: string }) {
|
||||
commands.deletePage(data, payload.id)
|
||||
},
|
||||
|
||||
/* --------------------- Shapes --------------------- */
|
||||
createShape(data, payload, type: ShapeType) {
|
||||
|
@ -1325,7 +1336,7 @@ const state = createState({
|
|||
},
|
||||
|
||||
restoreSavedData(data) {
|
||||
history.load(data)
|
||||
// history.load(data)
|
||||
},
|
||||
|
||||
clearBoundsRotation(data) {
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -1314,6 +1314,19 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-context-menu@^0.0.19":
|
||||
version "0.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-0.0.19.tgz#47ef194d7bc925ff7f998889be9fd373ce4bd9a1"
|
||||
integrity sha512-FR2jXeFqxD9n+1AC81+7jfMbnz80kdQNMfgIcPQL1S0M5SODADdDiTKKfok4Blc1nYWe7nEwBTYauMirF7avSQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "0.0.5"
|
||||
"@radix-ui/react-context" "0.0.5"
|
||||
"@radix-ui/react-menu" "0.0.18"
|
||||
"@radix-ui/react-polymorphic" "0.0.11"
|
||||
"@radix-ui/react-primitive" "0.0.13"
|
||||
"@radix-ui/react-use-callback-ref" "0.0.5"
|
||||
|
||||
"@radix-ui/react-context@0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-0.0.5.tgz#7c15f46795d7765dabfaf6f9c53791ad28c521c2"
|
||||
|
@ -1321,6 +1334,27 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-dialog@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.17.tgz#736e37626c4e9e20d46546ddbbb8869a352578f0"
|
||||
integrity sha512-DOSW8SdniyVLro+MvF0owaEEa8MUYGMuvuuSQpFnD/hA+K0pKzyTSjEuC7OeflPCImBFEbmKhgRoeWypfMZZOA==
|
||||
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.13"
|
||||
"@radix-ui/react-focus-guards" "0.0.7"
|
||||
"@radix-ui/react-focus-scope" "0.0.13"
|
||||
"@radix-ui/react-id" "0.0.6"
|
||||
"@radix-ui/react-polymorphic" "0.0.11"
|
||||
"@radix-ui/react-portal" "0.0.13"
|
||||
"@radix-ui/react-presence" "0.0.14"
|
||||
"@radix-ui/react-primitive" "0.0.13"
|
||||
"@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.13":
|
||||
version "0.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.13.tgz#7c4be6170a14d8a66c48680a8a8c987bc29bcf05"
|
||||
|
|
Loading…
Reference in a new issue