Added page rearranging by dragging (desktop only) (#768)
* Added page rearranging by dragging (desktop only) * Increment page names correctly, create drop indicator Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
b9a4a6c36e
commit
489b5a1001
9 changed files with 203 additions and 9 deletions
|
@ -1485,6 +1485,24 @@ left past the initial left edge) then swap points on that axis.
|
|||
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
|
||||
return Utils.isDarwin() ? e.metaKey : e.ctrlKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an incremented name (e.g. New page (2)) from a name (e.g. New page), based on an array of existing names.
|
||||
*
|
||||
* @param name The name to increment.
|
||||
* @param others The array of existing names.
|
||||
*/
|
||||
static getIncrementedName(name: string, others: string[]) {
|
||||
let result = name
|
||||
|
||||
while (others.includes(result)) {
|
||||
result = /\s\((\d+)\)$/.exec(result)?.[1]
|
||||
? result.replace(/\d+(?=\)$)/, (m) => (+m + 1).toString())
|
||||
: `${result} (1)`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export default Utils
|
||||
|
|
|
@ -77,19 +77,68 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
|
|||
[app]
|
||||
)
|
||||
|
||||
const [dragId, setDragId] = React.useState<null | string>(null)
|
||||
|
||||
const [dropIndex, setDropIndex] = React.useState<null | number>(null)
|
||||
|
||||
const handleDragStart = React.useCallback((ev: React.DragEvent<HTMLDivElement>) => {
|
||||
setDragId(ev.currentTarget.id)
|
||||
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
}, [])
|
||||
|
||||
const handleDrag = React.useCallback(
|
||||
(ev: React.DragEvent<HTMLDivElement>) => {
|
||||
ev.preventDefault()
|
||||
|
||||
const dropBox = sortedPages.find((p) => p.id === ev.currentTarget.id)
|
||||
|
||||
if (!dropBox) return
|
||||
|
||||
const indices = sortedPages.map((p) => p.childIndex ?? 0).sort()
|
||||
const index = indices.indexOf(dropBox.childIndex ?? 0)
|
||||
|
||||
const rect = ev.currentTarget.getBoundingClientRect()
|
||||
const ny = (ev.clientY - rect.top) / rect.height
|
||||
const dropIndex = ny < 0.5 ? index : index + 1
|
||||
|
||||
setDropIndex(dropIndex)
|
||||
},
|
||||
[dragId, sortedPages]
|
||||
)
|
||||
|
||||
const handleDrop = React.useCallback(() => {
|
||||
if (dragId !== null && dropIndex !== null) {
|
||||
app.movePage(dragId, dropIndex)
|
||||
}
|
||||
|
||||
setDragId(null)
|
||||
setDropIndex(null)
|
||||
}, [dragId, dropIndex])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.RadioGroup dir="ltr" value={currentPageId} onValueChange={handleChangePage}>
|
||||
{sortedPages.map((page) => (
|
||||
<ButtonWithOptions key={page.id}>
|
||||
{sortedPages.map((page, i) => (
|
||||
<ButtonWithOptions
|
||||
key={page.id}
|
||||
isDropAbove={i === dropIndex && i === 0}
|
||||
isDropBelow={dropIndex !== null && i === dropIndex - 1}
|
||||
>
|
||||
<DropdownMenu.RadioItem
|
||||
title={page.name || 'Page'}
|
||||
value={page.id}
|
||||
key={page.id}
|
||||
id={page.id}
|
||||
asChild
|
||||
onDragOver={handleDrag}
|
||||
onDragStart={handleDragStart}
|
||||
// onDrag={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
draggable={true}
|
||||
>
|
||||
<PageButton>
|
||||
<span>{page.name || 'Page'}</span>
|
||||
<span id={page.id}>{page.name || 'Page'}</span>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<SmallIcon>
|
||||
<CheckIcon />
|
||||
|
@ -117,9 +166,11 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
|
|||
}
|
||||
|
||||
const ButtonWithOptions = styled('div', {
|
||||
position: 'relative',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
gridAutoFlow: 'column',
|
||||
margin: 0,
|
||||
|
||||
'& > *[data-shy="true"]': {
|
||||
opacity: 0,
|
||||
|
@ -128,6 +179,39 @@ const ButtonWithOptions = styled('div', {
|
|||
'&:hover > *[data-shy="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
variants: {
|
||||
isDropAbove: {
|
||||
true: {
|
||||
'&::after': {
|
||||
content: '',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
backgroundColor: '$selected',
|
||||
zIndex: 999,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
isDropBelow: {
|
||||
true: {
|
||||
'&::after': {
|
||||
content: '',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
top: '100%',
|
||||
backgroundColor: '$selected',
|
||||
zIndex: 999,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const PageButton = styled(RowButton, {
|
||||
|
|
|
@ -1743,6 +1743,19 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
return this.setState(Commands.changePage(this, pageId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a page above another.
|
||||
* @param pageId The page to move.
|
||||
* @param index The page above which to move.
|
||||
*/
|
||||
movePage = (pageId: string, index: number): this => {
|
||||
if (this.readOnly) return this
|
||||
|
||||
if (this.page.childIndex === index) return this
|
||||
|
||||
return this.setState(Commands.movePage(this, pageId, index))
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a page.
|
||||
* @param pageId The id of the page to rename.
|
||||
|
|
|
@ -406,6 +406,7 @@ TldrawTestApp {
|
|||
"migrate": [Function],
|
||||
"moveBackward": [Function],
|
||||
"moveForward": [Function],
|
||||
"movePage": [Function],
|
||||
"movePointer": [Function],
|
||||
"moveToBack": [Function],
|
||||
"moveToFront": [Function],
|
||||
|
|
|
@ -30,4 +30,34 @@ describe('Create page command', () => {
|
|||
expect(app.page.id).toBe(nextId)
|
||||
expect(app.pageState).toEqual(nextPageState)
|
||||
})
|
||||
|
||||
it('increments page names', () => {
|
||||
app.loadDocument(mockDocument)
|
||||
|
||||
app.createPage()
|
||||
|
||||
expect(app.page.name).toBe('New page')
|
||||
|
||||
app.createPage()
|
||||
|
||||
expect(app.page.name).toBe('New page (1)')
|
||||
|
||||
app.createPage()
|
||||
|
||||
expect(app.page.name).toBe('New page (2)')
|
||||
|
||||
app.renamePage(app.page.id, 'New page!')
|
||||
|
||||
app.createPage()
|
||||
|
||||
expect(app.page.name).toBe('New page (2)')
|
||||
|
||||
app.deletePage(app.page.id)
|
||||
|
||||
expect(app.page.name).toBe('New page!')
|
||||
|
||||
app.createPage(undefined, 'New page!')
|
||||
|
||||
expect(app.page.name).toBe('New page! (1)')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,17 +10,20 @@ export function createPage(
|
|||
): TldrawCommand {
|
||||
const { currentPageId } = app
|
||||
|
||||
const topPage = Object.values(app.state.document.pages).sort(
|
||||
(a, b) => (b.childIndex || 0) - (a.childIndex || 0)
|
||||
)[0]
|
||||
const pages = Object.values(app.state.document.pages).sort(
|
||||
(a, b) => (a.childIndex ?? 0) - (b.childIndex ?? 0)
|
||||
)
|
||||
|
||||
const topPage = pages[pages.length - 1]
|
||||
|
||||
const nextChildIndex = topPage?.childIndex ? topPage?.childIndex + 1 : 1
|
||||
|
||||
// TODO: Iterate the name better
|
||||
|
||||
const page: TDPage = {
|
||||
id: pageId,
|
||||
name: pageName,
|
||||
name: Utils.getIncrementedName(
|
||||
pageName,
|
||||
pages.map((p) => p.name ?? '')
|
||||
),
|
||||
childIndex: nextChildIndex,
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
|
|
|
@ -10,6 +10,7 @@ export * from './duplicateShapes'
|
|||
export * from './flipShapes'
|
||||
export * from './groupShapes'
|
||||
export * from './moveShapesToPage'
|
||||
export * from './movePage'
|
||||
export * from './reorderShapes'
|
||||
export * from './renamePage'
|
||||
export * from './resetBounds'
|
||||
|
|
1
packages/tldraw/src/state/commands/movePage/index.ts
Normal file
1
packages/tldraw/src/state/commands/movePage/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './movePage'
|
43
packages/tldraw/src/state/commands/movePage/movePage.ts
Normal file
43
packages/tldraw/src/state/commands/movePage/movePage.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import type { TDPage, TldrawCommand } from '~types'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
|
||||
export function movePage(app: TldrawApp, pageId: string, index: number): TldrawCommand {
|
||||
const { pages } = app.document
|
||||
const currentIndex = pages[pageId].childIndex ?? 0
|
||||
const movingUp = index < currentIndex
|
||||
const startToMove = Math.min(index, currentIndex)
|
||||
const endToMove = Math.max(index, currentIndex)
|
||||
|
||||
const pagesToMove = Object.values(pages).filter(
|
||||
(page) =>
|
||||
page.childIndex !== undefined &&
|
||||
page.childIndex <= endToMove &&
|
||||
page.childIndex >= startToMove
|
||||
)
|
||||
|
||||
return {
|
||||
id: 'move_page',
|
||||
before: {
|
||||
document: {
|
||||
pages: Object.fromEntries(
|
||||
pagesToMove.map((p: TDPage) => {
|
||||
return [p.id, { childIndex: p.childIndex }]
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: Object.fromEntries(
|
||||
pagesToMove.map((p) => {
|
||||
if (p.childIndex == undefined) return [p.id, { childIndex: p.childIndex }]
|
||||
return [
|
||||
p.id,
|
||||
{ childIndex: p.id == pageId ? index : p.childIndex + (movingUp ? 1 : -1) },
|
||||
]
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue