Adds create / delete pages

This commit is contained in:
Steve Ruiz 2021-06-03 14:10:54 +01:00
parent 3b74580b4f
commit 507c081bd0
9 changed files with 301 additions and 28 deletions

View file

@ -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 }) => (
<ContextMenu.Root key={id}>
<ContextMenu.Trigger>
<StyledRadioItem key={id} value={id}>
<span>{name}</span>
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
<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',
},
})

View file

@ -78,21 +78,10 @@ export const RowButton = styled('button', {
fontSize: '$1',
justifyContent: 'space-between',
padding: '4px 6px 4px 12px',
'&::before': {
content: "''",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
zIndex: -1,
},
'&:hover::before': {
backgroundColor: '$hover',
borderRadius: 4,
'&:hover': {
backgroundColor: '$hover',
},
'& label': {

View file

@ -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",

View file

@ -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(

View 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,
}
}

View 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,
}
}

View file

@ -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,

View file

@ -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) {

View file

@ -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"