Adds alignment to context menu
This commit is contained in:
parent
396ff60301
commit
946fdbab4c
7 changed files with 192 additions and 14 deletions
|
@ -11,7 +11,7 @@ import BoundsBg from './bounds/bounds-bg'
|
||||||
import Selected from './selected'
|
import Selected from './selected'
|
||||||
import Handles from './bounds/handles'
|
import Handles from './bounds/handles'
|
||||||
import useCanvasEvents from 'hooks/useCanvasEvents'
|
import useCanvasEvents from 'hooks/useCanvasEvents'
|
||||||
import ContextMenu from 'components/context-menu'
|
import ContextMenu from './context-menu/context-menu'
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import * as _ContextMenu from '@radix-ui/react-context-menu'
|
import * as _ContextMenu from '@radix-ui/react-context-menu'
|
||||||
import * as _Dropdown from '@radix-ui/react-dropdown-menu'
|
import * as _Dropdown from '@radix-ui/react-dropdown-menu'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { IconWrapper, RowButton } from './shared'
|
import {
|
||||||
|
IconWrapper,
|
||||||
|
IconButton as _IconButton,
|
||||||
|
RowButton,
|
||||||
|
} from 'components/shared'
|
||||||
import {
|
import {
|
||||||
commandKey,
|
commandKey,
|
||||||
deepCompareArrays,
|
deepCompareArrays,
|
||||||
|
@ -9,9 +13,67 @@ import {
|
||||||
isMobile,
|
isMobile,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import { MoveType, ShapeType } from 'types'
|
import {
|
||||||
|
AlignType,
|
||||||
|
DistributeType,
|
||||||
|
MoveType,
|
||||||
|
ShapeType,
|
||||||
|
StretchType,
|
||||||
|
} from 'types'
|
||||||
import React, { useRef } from 'react'
|
import React, { useRef } from 'react'
|
||||||
import { ChevronRightIcon } from '@radix-ui/react-icons'
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
AlignBottomIcon,
|
||||||
|
AlignCenterHorizontallyIcon,
|
||||||
|
AlignCenterVerticallyIcon,
|
||||||
|
AlignLeftIcon,
|
||||||
|
AlignRightIcon,
|
||||||
|
AlignTopIcon,
|
||||||
|
SpaceEvenlyHorizontallyIcon,
|
||||||
|
SpaceEvenlyVerticallyIcon,
|
||||||
|
StretchHorizontallyIcon,
|
||||||
|
StretchVerticallyIcon,
|
||||||
|
} from '@radix-ui/react-icons'
|
||||||
|
|
||||||
|
function alignTop() {
|
||||||
|
state.send('ALIGNED', { type: AlignType.Top })
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignCenterVertical() {
|
||||||
|
state.send('ALIGNED', { type: AlignType.CenterVertical })
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignBottom() {
|
||||||
|
state.send('ALIGNED', { type: AlignType.Bottom })
|
||||||
|
}
|
||||||
|
|
||||||
|
function stretchVertically() {
|
||||||
|
state.send('STRETCHED', { type: StretchType.Vertical })
|
||||||
|
}
|
||||||
|
|
||||||
|
function distributeVertically() {
|
||||||
|
state.send('DISTRIBUTED', { type: DistributeType.Vertical })
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignLeft() {
|
||||||
|
state.send('ALIGNED', { type: AlignType.Left })
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignCenterHorizontal() {
|
||||||
|
state.send('ALIGNED', { type: AlignType.CenterHorizontal })
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignRight() {
|
||||||
|
state.send('ALIGNED', { type: AlignType.Right })
|
||||||
|
}
|
||||||
|
|
||||||
|
function stretchHorizontally() {
|
||||||
|
state.send('STRETCHED', { type: StretchType.Horizontal })
|
||||||
|
}
|
||||||
|
|
||||||
|
function distributeHorizontally() {
|
||||||
|
state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
|
||||||
|
}
|
||||||
|
|
||||||
export default function ContextMenu({
|
export default function ContextMenu({
|
||||||
children,
|
children,
|
||||||
|
@ -26,7 +88,8 @@ export default function ContextMenu({
|
||||||
const rContent = useRef<HTMLDivElement>(null)
|
const rContent = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
|
const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
|
||||||
const hasMultipleSelected = selectedShapes.length > 1
|
const hasTwoOrMore = selectedShapes.length > 1
|
||||||
|
const hasThreeOrMore = selectedShapes.length > 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<_ContextMenu.Root>
|
<_ContextMenu.Root>
|
||||||
|
@ -58,7 +121,7 @@ export default function ContextMenu({
|
||||||
</Button>
|
</Button>
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
{hasGroupSelectd ||
|
{hasGroupSelectd ||
|
||||||
(hasMultipleSelected && (
|
(hasTwoOrMore && (
|
||||||
<>
|
<>
|
||||||
{hasGroupSelectd && (
|
{hasGroupSelectd && (
|
||||||
<Button onSelect={() => state.send('UNGROUPED')}>
|
<Button onSelect={() => state.send('UNGROUPED')}>
|
||||||
|
@ -70,7 +133,7 @@ export default function ContextMenu({
|
||||||
</kbd>
|
</kbd>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{hasMultipleSelected && (
|
{hasTwoOrMore && (
|
||||||
<Button onSelect={() => state.send('GROUPED')}>
|
<Button onSelect={() => state.send('GROUPED')}>
|
||||||
<span>Group</span>
|
<span>Group</span>
|
||||||
<kbd>
|
<kbd>
|
||||||
|
@ -138,6 +201,12 @@ export default function ContextMenu({
|
||||||
</kbd>
|
</kbd>
|
||||||
</Button>
|
</Button>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
{hasTwoOrMore && (
|
||||||
|
<AlignDistributeSubMenu
|
||||||
|
hasTwoOrMore={hasTwoOrMore}
|
||||||
|
hasThreeOrMore={hasThreeOrMore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MoveToPageMenu />
|
<MoveToPageMenu />
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
<Button onSelect={() => state.send('DELETED')}>
|
<Button onSelect={() => state.send('DELETED')}>
|
||||||
|
@ -232,6 +301,27 @@ function Button({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconButton({
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
onSelect: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<_ContextMenu.Item
|
||||||
|
as={_IconButton}
|
||||||
|
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||||
|
disabled={disabled}
|
||||||
|
onSelect={onSelect}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</_ContextMenu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function SubMenu({
|
function SubMenu({
|
||||||
children,
|
children,
|
||||||
label,
|
label,
|
||||||
|
@ -258,6 +348,85 @@ function SubMenu({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AlignDistributeSubMenu({
|
||||||
|
hasTwoOrMore,
|
||||||
|
hasThreeOrMore,
|
||||||
|
}: {
|
||||||
|
hasTwoOrMore: boolean
|
||||||
|
hasThreeOrMore: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<_ContextMenu.Root>
|
||||||
|
<_ContextMenu.TriggerItem
|
||||||
|
as={RowButton}
|
||||||
|
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||||
|
>
|
||||||
|
<span>Align / Distribute</span>
|
||||||
|
<IconWrapper size="small">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</IconWrapper>
|
||||||
|
</_ContextMenu.TriggerItem>
|
||||||
|
<StyledGrid
|
||||||
|
sideOffset={2}
|
||||||
|
alignOffset={-2}
|
||||||
|
isMobile={isMobile()}
|
||||||
|
selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}
|
||||||
|
>
|
||||||
|
<IconButton onSelect={alignLeft}>
|
||||||
|
<AlignLeftIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onSelect={alignCenterHorizontal}>
|
||||||
|
<AlignCenterHorizontallyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onSelect={alignRight}>
|
||||||
|
<AlignRightIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onSelect={stretchHorizontally}>
|
||||||
|
<StretchHorizontallyIcon />
|
||||||
|
</IconButton>
|
||||||
|
{hasThreeOrMore && (
|
||||||
|
<IconButton onSelect={distributeHorizontally}>
|
||||||
|
<SpaceEvenlyHorizontallyIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconButton onSelect={alignTop}>
|
||||||
|
<AlignTopIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onSelect={alignCenterVertical}>
|
||||||
|
<AlignCenterVerticallyIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onSelect={alignBottom}>
|
||||||
|
<AlignBottomIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onSelect={stretchVertically}>
|
||||||
|
<StretchVerticallyIcon />
|
||||||
|
</IconButton>
|
||||||
|
{hasThreeOrMore && (
|
||||||
|
<IconButton onSelect={distributeVertically}>
|
||||||
|
<SpaceEvenlyVerticallyIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
<StyledArrow offset={13} />
|
||||||
|
</StyledGrid>
|
||||||
|
</_ContextMenu.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledGrid = styled(StyledContent, {
|
||||||
|
display: 'grid',
|
||||||
|
variants: {
|
||||||
|
selectedStyle: {
|
||||||
|
threeOrMore: {
|
||||||
|
gridTemplateColumns: 'repeat(5, auto)',
|
||||||
|
},
|
||||||
|
twoOrMore: {
|
||||||
|
gridTemplateColumns: 'repeat(4, auto)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
function MoveToPageMenu() {
|
function MoveToPageMenu() {
|
||||||
const documentPages = useSelector((s) => s.data.document.pages)
|
const documentPages = useSelector((s) => s.data.document.pages)
|
||||||
const currentPageId = useSelector((s) => s.data.currentPageId)
|
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||||
|
@ -268,6 +437,8 @@ function MoveToPageMenu() {
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
.filter((a) => a.id !== currentPageId)
|
.filter((a) => a.id !== currentPageId)
|
||||||
|
|
||||||
|
if (sorted.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<_ContextMenu.Root>
|
<_ContextMenu.Root>
|
||||||
<_ContextMenu.TriggerItem
|
<_ContextMenu.TriggerItem
|
|
@ -7,7 +7,7 @@ import { ShapeStyles, ShapeType } from 'types'
|
||||||
import useShapeEvents from 'hooks/useShapeEvents'
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { getShapeStyle } from 'lib/shape-styles'
|
import { getShapeStyle } from 'lib/shape-styles'
|
||||||
import ContextMenu from 'components/context-menu'
|
import ContextMenu from 'components/canvas/context-menu/context-menu'
|
||||||
|
|
||||||
interface ShapeProps {
|
interface ShapeProps {
|
||||||
id: string
|
id: string
|
||||||
|
|
|
@ -9,7 +9,7 @@ import StylePanel from './style-panel/style-panel'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import PagePanel from './page-panel/page-panel'
|
import PagePanel from './page-panel/page-panel'
|
||||||
import ContextMenu from './context-menu'
|
import ContextMenu from './canvas/context-menu/context-menu'
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
useKeyboardEvents()
|
useKeyboardEvents()
|
||||||
|
|
|
@ -2,7 +2,6 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
import * as Panel from './panel'
|
import * as Panel from './panel'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import Tooltip from './tooltip'
|
|
||||||
|
|
||||||
export const IconButton = styled('button', {
|
export const IconButton = styled('button', {
|
||||||
height: '32px',
|
height: '32px',
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { uniqueId } from 'utils/utils'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import storage from 'state/storage'
|
import storage from 'state/storage'
|
||||||
|
|
||||||
export default function createPage(data: Data) {
|
export default function createPage(data: Data, goToPage = true) {
|
||||||
const snapshot = getSnapshot(data)
|
const snapshot = getSnapshot(data)
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
|
@ -14,12 +14,19 @@ export default function createPage(data: Data) {
|
||||||
name: 'change_page',
|
name: 'change_page',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { page, pageState } = snapshot
|
const { page, pageState, currentPageId } = snapshot
|
||||||
data.document.pages[page.id] = page
|
data.document.pages[page.id] = page
|
||||||
data.pageStates[page.id] = pageState
|
data.pageStates[page.id] = pageState
|
||||||
|
|
||||||
data.currentPageId = page.id
|
data.currentPageId = page.id
|
||||||
storage.savePage(data, data.document.id, page.id)
|
storage.savePage(data, data.document.id, page.id)
|
||||||
storage.saveDocumentToLocalStorage(data)
|
storage.saveDocumentToLocalStorage(data)
|
||||||
|
|
||||||
|
if (goToPage) {
|
||||||
|
data.currentPageId = page.id
|
||||||
|
} else {
|
||||||
|
data.currentPageId = currentPageId
|
||||||
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { page, currentPageId } = snapshot
|
const { page, currentPageId } = snapshot
|
||||||
|
@ -46,6 +53,7 @@ function getSnapshot(data: Data) {
|
||||||
childIndex: pages.length,
|
childIndex: pages.length,
|
||||||
shapes: {},
|
shapes: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageState: PageState = {
|
const pageState: PageState = {
|
||||||
id,
|
id,
|
||||||
selectedIds: new Set([]),
|
selectedIds: new Set([]),
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
getPageState,
|
getPageState,
|
||||||
getShapes,
|
getShapes,
|
||||||
|
setToArray,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
|
@ -1001,12 +1002,11 @@ const state = createState({
|
||||||
commands.changePage(data, payload.id)
|
commands.changePage(data, payload.id)
|
||||||
},
|
},
|
||||||
createPage(data) {
|
createPage(data) {
|
||||||
commands.createPage(data)
|
commands.createPage(data, true)
|
||||||
},
|
},
|
||||||
deletePage(data, payload: { id: string }) {
|
deletePage(data, payload: { id: string }) {
|
||||||
commands.deletePage(data, payload.id)
|
commands.deletePage(data, payload.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
/* --------------------- Shapes --------------------- */
|
/* --------------------- Shapes --------------------- */
|
||||||
resetShapes(data) {
|
resetShapes(data) {
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue