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:
Enrico 2022-06-29 11:25:00 +02:00 committed by GitHub
parent b9a4a6c36e
commit 489b5a1001
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 203 additions and 9 deletions

View file

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

View file

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

View file

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

View file

@ -406,6 +406,7 @@ TldrawTestApp {
"migrate": [Function],
"moveBackward": [Function],
"moveForward": [Function],
"movePage": [Function],
"movePointer": [Function],
"moveToBack": [Function],
"moveToFront": [Function],

View file

@ -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)')
})
})

View file

@ -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: {},

View file

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

View file

@ -0,0 +1 @@
export * from './movePage'

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