kopia lustrzana https://github.com/Tldraw/Tldraw
235 wiersze
7.1 KiB
TypeScript
235 wiersze
7.1 KiB
TypeScript
import { TLParentId, TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'
|
|
import { IndexKey, compact, getIndicesBetween, sortByIndex } from '@tldraw/utils'
|
|
import { Editor } from '../editor/Editor'
|
|
|
|
export function getReorderingShapesChanges(
|
|
editor: Editor,
|
|
operation: 'toBack' | 'toFront' | 'forward' | 'backward',
|
|
ids: TLShapeId[]
|
|
) {
|
|
if (ids.length === 0) return []
|
|
|
|
// From the ids that are moving, collect the parents, their children, and which of those children are moving
|
|
const parents = new Map<TLParentId, { moving: Set<TLShape>; children: TLShape[] }>()
|
|
|
|
for (const shape of compact(ids.map((id) => editor.getShape(id)))) {
|
|
const { parentId } = shape
|
|
if (!parents.has(parentId)) {
|
|
parents.set(parentId, {
|
|
children: compact(
|
|
editor.getSortedChildIdsForParent(parentId).map((id) => editor.getShape(id))
|
|
),
|
|
moving: new Set(),
|
|
})
|
|
}
|
|
parents.get(parentId)!.moving.add(shape)
|
|
}
|
|
|
|
const changes: TLShapePartial[] = []
|
|
|
|
switch (operation) {
|
|
case 'toBack': {
|
|
parents.forEach(({ moving, children }) => reorderToBack(moving, children, changes))
|
|
break
|
|
}
|
|
case 'toFront': {
|
|
parents.forEach(({ moving, children }) => reorderToFront(moving, children, changes))
|
|
break
|
|
}
|
|
case 'forward': {
|
|
parents.forEach(({ moving, children }) => reorderForward(moving, children, changes))
|
|
break
|
|
}
|
|
case 'backward': {
|
|
parents.forEach(({ moving, children }) => reorderBackward(moving, children, changes))
|
|
break
|
|
}
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
/**
|
|
* Reorders the moving shapes to the back of the parent's children.
|
|
*
|
|
* @param moving The set of shapes that are moving
|
|
* @param children The parent's children
|
|
* @param changes The changes array to push changes to
|
|
*/
|
|
function reorderToBack(moving: Set<TLShape>, children: TLShape[], changes: TLShapePartial[]) {
|
|
const len = children.length
|
|
|
|
// If all of the children are moving, there's nothing to do
|
|
if (moving.size === len) return
|
|
|
|
let below: IndexKey | undefined
|
|
let above: IndexKey | undefined
|
|
|
|
// Starting at the bottom of this parent's children...
|
|
for (let i = 0; i < len; i++) {
|
|
const shape = children[i]
|
|
|
|
if (moving.has(shape)) {
|
|
// If we've found a moving shape before we've found a non-moving shape,
|
|
// then that shape is already at the back; we can remove it from the
|
|
// moving set and mark it as the shape that will be below the moved shapes.
|
|
below = shape.index
|
|
moving.delete(shape)
|
|
} else {
|
|
// The first non-moving shape we find will be above our moved shapes; we'll
|
|
// put our moving shapes between it and the shape marked as below (if any).
|
|
above = shape.index
|
|
break
|
|
}
|
|
}
|
|
|
|
if (moving.size === 0) {
|
|
// If our moving set is empty, there's nothing to do; all of our shapes were
|
|
// already at the back of the parent's children.
|
|
return
|
|
} else {
|
|
// Sort the moving shapes by their current index, then apply the new indices
|
|
const indices = getIndicesBetween(below, above, moving.size)
|
|
changes.push(
|
|
...Array.from(moving.values())
|
|
.sort(sortByIndex)
|
|
.map((shape, i) => ({ ...shape, index: indices[i] }))
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reorders the moving shapes to the front of the parent's children.
|
|
*
|
|
* @param moving The set of shapes that are moving
|
|
* @param children The parent's children
|
|
* @param changes The changes array to push changes to
|
|
*/
|
|
function reorderToFront(moving: Set<TLShape>, children: TLShape[], changes: TLShapePartial[]) {
|
|
const len = children.length
|
|
|
|
// If all of the children are moving, there's nothing to do
|
|
if (moving.size === len) return
|
|
|
|
let below: IndexKey | undefined
|
|
let above: IndexKey | undefined
|
|
|
|
// Starting at the top of this parent's children...
|
|
for (let i = len - 1; i > -1; i--) {
|
|
const shape = children[i]
|
|
|
|
if (moving.has(shape)) {
|
|
// If we've found a moving shape before we've found a non-moving shape,
|
|
// then that shape is already at the front; we can remove it from the
|
|
// moving set and mark it as the shape that will be above the moved shapes.
|
|
above = shape.index
|
|
moving.delete(shape)
|
|
} else {
|
|
// The first non-moving shape we find will be below our moved shapes; we'll
|
|
// put our moving shapes between it and the shape marked as above (if any).
|
|
below = shape.index
|
|
break
|
|
}
|
|
}
|
|
|
|
if (moving.size === 0) {
|
|
// If our moving set is empty, there's nothing to do; all of our shapes were
|
|
// already at the front of the parent's children.
|
|
return
|
|
} else {
|
|
// Sort the moving shapes by their current index, then apply the new indices
|
|
const indices = getIndicesBetween(below, above, moving.size)
|
|
changes.push(
|
|
...Array.from(moving.values())
|
|
.sort(sortByIndex)
|
|
.map((shape, i) => ({ ...shape, index: indices[i] }))
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reorders the moving shapes forward in the parent's children.
|
|
*
|
|
* @param moving The set of shapes that are moving
|
|
* @param children The parent's children
|
|
* @param changes The changes array to push changes to
|
|
*/
|
|
function reorderForward(moving: Set<TLShape>, children: TLShape[], changes: TLShapePartial[]) {
|
|
const len = children.length
|
|
|
|
// If all of the children are moving, there's nothing to do
|
|
if (moving.size === len) return
|
|
|
|
let state = { name: 'skipping' } as
|
|
| { name: 'skipping' }
|
|
| { name: 'selecting'; selectIndex: number }
|
|
|
|
// Starting at the bottom of this parent's children...
|
|
for (let i = 0; i < len; i++) {
|
|
const isMoving = moving.has(children[i])
|
|
|
|
switch (state.name) {
|
|
case 'skipping': {
|
|
if (!isMoving) continue
|
|
// If we find a moving shape while skipping, start selecting
|
|
state = { name: 'selecting', selectIndex: i }
|
|
break
|
|
}
|
|
case 'selecting': {
|
|
if (isMoving) continue
|
|
// if we find a non-moving shape while selecting, move all selected
|
|
// shapes in front of the not moving shape; and start skipping
|
|
const { selectIndex } = state
|
|
getIndicesBetween(children[i].index, children[i + 1]?.index, i - selectIndex).forEach(
|
|
(index, k) => changes.push({ ...children[selectIndex + k], index })
|
|
)
|
|
state = { name: 'skipping' }
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reorders the moving shapes backward in the parent's children.
|
|
*
|
|
* @param moving The set of shapes that are moving
|
|
* @param children The parent's children
|
|
* @param changes The changes array to push changes to
|
|
*/
|
|
function reorderBackward(moving: Set<TLShape>, children: TLShape[], changes: TLShapePartial[]) {
|
|
const len = children.length
|
|
|
|
if (moving.size === len) return
|
|
|
|
let state = { name: 'skipping' } as
|
|
| { name: 'skipping' }
|
|
| { name: 'selecting'; selectIndex: number }
|
|
|
|
// Starting at the top of this parent's children...
|
|
for (let i = len - 1; i > -1; i--) {
|
|
const isMoving = moving.has(children[i])
|
|
|
|
switch (state.name) {
|
|
case 'skipping': {
|
|
if (!isMoving) continue
|
|
// If we find a moving shape while skipping, start selecting
|
|
state = { name: 'selecting', selectIndex: i }
|
|
break
|
|
}
|
|
case 'selecting': {
|
|
if (isMoving) continue
|
|
// if we find a non-moving shape while selecting, move all selected
|
|
// shapes in behind of the not moving shape; and start skipping
|
|
getIndicesBetween(children[i - 1]?.index, children[i].index, state.selectIndex - i).forEach(
|
|
(index, k) => {
|
|
changes.push({ ...children[i + k + 1], index })
|
|
}
|
|
)
|
|
state = { name: 'skipping' }
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|