[refactor] reordering shapes (#1718)
This PR: - adds tests for shape reordering - removes `Editor.getParentsMappedToChildren` - removes `Editor.reorderShapes` - moves reordering shapes code into its own file, outside of the editor ### Change Type - [x] `major` — Breaking change (if you were using those APIs) ### Release Notes - [api] removes `Editor.getParentsMappedToChildren` - [api] removes `Editor.reorderShapes` - [api] moves reordering shapes code into its own file, outside of the editor
This commit is contained in:
parent
85db9ba469
commit
103809b83e
5 changed files with 1150 additions and 219 deletions
|
@ -503,7 +503,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getPageTransformById(id: TLShapeId): Matrix2d | undefined;
|
getPageTransformById(id: TLShapeId): Matrix2d | undefined;
|
||||||
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId;
|
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId;
|
||||||
getParentShape(shape?: TLShape): TLShape | undefined;
|
getParentShape(shape?: TLShape): TLShape | undefined;
|
||||||
getParentsMappedToChildren(ids: TLShapeId[]): Map<TLParentId, Set<TLShape>>;
|
|
||||||
getParentTransform(shape: TLShape): Matrix2d;
|
getParentTransform(shape: TLShape): Matrix2d;
|
||||||
getPointInParentSpace(shapeId: TLShapeId, point: VecLike): Vec2d;
|
getPointInParentSpace(shapeId: TLShapeId, point: VecLike): Vec2d;
|
||||||
getPointInShapeSpace(shape: TLShape, point: VecLike): Vec2d;
|
getPointInShapeSpace(shape: TLShape, point: VecLike): Vec2d;
|
||||||
|
@ -631,7 +630,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
isInViewport: boolean;
|
isInViewport: boolean;
|
||||||
maskedPageBounds: Box2d | undefined;
|
maskedPageBounds: Box2d | undefined;
|
||||||
}[];
|
}[];
|
||||||
reorderShapes(operation: 'backward' | 'forward' | 'toBack' | 'toFront', ids: TLShapeId[]): this;
|
|
||||||
reparentShapesById(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
|
reparentShapesById(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
|
||||||
replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]): void;
|
replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]): void;
|
||||||
resetZoom(point?: Vec2d, opts?: TLAnimationOptions): this;
|
resetZoom(point?: Vec2d, opts?: TLAnimationOptions): this;
|
||||||
|
|
|
@ -111,6 +111,7 @@ import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/Sh
|
||||||
import { WeakMapCache } from '../utils/WeakMapCache'
|
import { WeakMapCache } from '../utils/WeakMapCache'
|
||||||
import { dataUrlToFile } from '../utils/assets'
|
import { dataUrlToFile } from '../utils/assets'
|
||||||
import { getIncrementedName, uniqueId } from '../utils/data'
|
import { getIncrementedName, uniqueId } from '../utils/data'
|
||||||
|
import { getReorderingShapesChanges } from '../utils/reorderShapes'
|
||||||
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
||||||
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
|
||||||
import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes'
|
import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes'
|
||||||
|
@ -5148,31 +5149,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this.getDeltaInShapeSpace(parent, delta)
|
return this.getDeltaInShapeSpace(parent, delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For a given set of ids, get a map containing the ids of their parents and the children of those
|
|
||||||
* parents.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* editor.getParentsMappedToChildren(['id1', 'id2', 'id3'])
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param ids - The ids to get the parents and children of.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
getParentsMappedToChildren(ids: TLShapeId[]) {
|
|
||||||
const shapes = ids.map((id) => this.store.get(id)!)
|
|
||||||
const parents = new Map<TLParentId, Set<TLShape>>()
|
|
||||||
shapes.forEach((shape) => {
|
|
||||||
if (!parents.has(shape.parentId)) {
|
|
||||||
parents.set(shape.parentId, new Set())
|
|
||||||
}
|
|
||||||
parents.get(shape.parentId)?.add(shape)
|
|
||||||
})
|
|
||||||
return parents
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all of the shapes in the current page.
|
* An array containing all of the shapes in the current page.
|
||||||
*
|
*
|
||||||
|
@ -6014,179 +5990,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reorder shapes.
|
|
||||||
*
|
|
||||||
* @param operation - The operation to perform.
|
|
||||||
* @param ids - The ids to reorder.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
reorderShapes(operation: 'toBack' | 'toFront' | 'forward' | 'backward', ids: TLShapeId[]) {
|
|
||||||
if (this.isReadOnly) return this
|
|
||||||
if (ids.length === 0) return this
|
|
||||||
// this.emit('reorder-shapes', { pageId: this.currentPageId, ids, operation })
|
|
||||||
|
|
||||||
const parents = this.getParentsMappedToChildren(ids)
|
|
||||||
|
|
||||||
const changes: TLShapePartial[] = []
|
|
||||||
|
|
||||||
switch (operation) {
|
|
||||||
case 'toBack': {
|
|
||||||
parents.forEach((movingSet, parentId) => {
|
|
||||||
const siblings = compact(
|
|
||||||
this.getSortedChildIds(parentId).map((id) => this.getShapeById(id))
|
|
||||||
)
|
|
||||||
|
|
||||||
if (movingSet.size === siblings.length) return
|
|
||||||
|
|
||||||
let below: string | undefined
|
|
||||||
let above: string | undefined
|
|
||||||
|
|
||||||
for (const shape of siblings) {
|
|
||||||
if (!movingSet.has(shape)) {
|
|
||||||
above = shape.index
|
|
||||||
break
|
|
||||||
}
|
|
||||||
movingSet.delete(shape)
|
|
||||||
below = shape.index
|
|
||||||
}
|
|
||||||
|
|
||||||
if (movingSet.size === 0) return
|
|
||||||
|
|
||||||
const indices = getIndicesBetween(below, above, movingSet.size)
|
|
||||||
|
|
||||||
Array.from(movingSet.values())
|
|
||||||
.sort(sortByIndex)
|
|
||||||
.forEach((node, i) =>
|
|
||||||
changes.push({ id: node.id as any, type: node.type, index: indices[i] })
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'toFront': {
|
|
||||||
parents.forEach((movingSet, parentId) => {
|
|
||||||
const siblings = compact(
|
|
||||||
this.getSortedChildIds(parentId).map((id) => this.getShapeById(id))
|
|
||||||
)
|
|
||||||
const len = siblings.length
|
|
||||||
|
|
||||||
if (movingSet.size === len) return
|
|
||||||
|
|
||||||
let below: string | undefined
|
|
||||||
let above: string | undefined
|
|
||||||
|
|
||||||
for (let i = len - 1; i > -1; i--) {
|
|
||||||
const shape = siblings[i]
|
|
||||||
|
|
||||||
if (!movingSet.has(shape)) {
|
|
||||||
below = shape.index
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
movingSet.delete(shape)
|
|
||||||
above = shape.index
|
|
||||||
}
|
|
||||||
|
|
||||||
if (movingSet.size === 0) return
|
|
||||||
|
|
||||||
const indices = getIndicesBetween(below, above, movingSet.size)
|
|
||||||
|
|
||||||
Array.from(movingSet.values())
|
|
||||||
.sort(sortByIndex)
|
|
||||||
.forEach((node, i) =>
|
|
||||||
changes.push({ id: node.id as any, type: node.type, index: indices[i] })
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'forward': {
|
|
||||||
parents.forEach((movingSet, parentId) => {
|
|
||||||
const siblings = compact(
|
|
||||||
this.getSortedChildIds(parentId).map((id) => this.getShapeById(id))
|
|
||||||
)
|
|
||||||
const len = siblings.length
|
|
||||||
|
|
||||||
if (movingSet.size === len) return
|
|
||||||
|
|
||||||
const movingIndices = new Set(Array.from(movingSet).map((n) => siblings.indexOf(n)))
|
|
||||||
|
|
||||||
let selectIndex = -1
|
|
||||||
let isSelecting = false
|
|
||||||
let below: string | undefined
|
|
||||||
let above: string | undefined
|
|
||||||
let count: number
|
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const isMoving = movingIndices.has(i)
|
|
||||||
|
|
||||||
if (!isSelecting && isMoving) {
|
|
||||||
isSelecting = true
|
|
||||||
selectIndex = i
|
|
||||||
above = undefined
|
|
||||||
} else if (isSelecting && !isMoving) {
|
|
||||||
isSelecting = false
|
|
||||||
count = i - selectIndex
|
|
||||||
below = siblings[i].index
|
|
||||||
above = siblings[i + 1]?.index
|
|
||||||
|
|
||||||
const indices = getIndicesBetween(below, above, count)
|
|
||||||
|
|
||||||
for (let k = 0; k < count; k++) {
|
|
||||||
const node = siblings[selectIndex + k]
|
|
||||||
changes.push({ id: node.id as any, type: node.type, index: indices[k] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'backward': {
|
|
||||||
parents.forEach((movingSet, parentId) => {
|
|
||||||
const siblings = compact(
|
|
||||||
this.getSortedChildIds(parentId).map((id) => this.getShapeById(id))
|
|
||||||
)
|
|
||||||
const len = siblings.length
|
|
||||||
|
|
||||||
if (movingSet.size === len) return
|
|
||||||
|
|
||||||
const movingIndices = new Set(Array.from(movingSet).map((n) => siblings.indexOf(n)))
|
|
||||||
|
|
||||||
let selectIndex = -1
|
|
||||||
let isSelecting = false
|
|
||||||
let count: number
|
|
||||||
|
|
||||||
for (let i = len - 1; i > -1; i--) {
|
|
||||||
const isMoving = movingIndices.has(i)
|
|
||||||
|
|
||||||
if (!isSelecting && isMoving) {
|
|
||||||
isSelecting = true
|
|
||||||
selectIndex = i
|
|
||||||
} else if (isSelecting && !isMoving) {
|
|
||||||
isSelecting = false
|
|
||||||
count = selectIndex - i
|
|
||||||
|
|
||||||
const indices = getIndicesBetween(siblings[i - 1]?.index, siblings[i].index, count)
|
|
||||||
|
|
||||||
for (let k = 0; k < count; k++) {
|
|
||||||
const node = siblings[i + k + 1]
|
|
||||||
changes.push({ id: node.id as any, type: node.type, index: indices[k] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateShapes(changes)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send shapes to the back of the page's object list.
|
* Send shapes to the back of the page's object list.
|
||||||
*
|
*
|
||||||
|
@ -6201,7 +6004,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
sendToBack(ids = this.pageState.selectedIds) {
|
sendToBack(ids = this.pageState.selectedIds) {
|
||||||
this.reorderShapes('toBack', ids)
|
const changes = getReorderingShapesChanges(this, 'toBack', ids)
|
||||||
|
if (changes) this.updateShapes(changes)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6219,7 +6023,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
sendBackward(ids = this.pageState.selectedIds) {
|
sendBackward(ids = this.pageState.selectedIds) {
|
||||||
this.reorderShapes('backward', ids)
|
const changes = getReorderingShapesChanges(this, 'backward', ids)
|
||||||
|
if (changes) this.updateShapes(changes)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6237,7 +6042,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
bringForward(ids = this.pageState.selectedIds) {
|
bringForward(ids = this.pageState.selectedIds) {
|
||||||
this.reorderShapes('forward', ids)
|
const changes = getReorderingShapesChanges(this, 'forward', ids)
|
||||||
|
if (changes) this.updateShapes(changes)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6255,7 +6061,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
bringToFront(ids = this.pageState.selectedIds) {
|
bringToFront(ids = this.pageState.selectedIds) {
|
||||||
this.reorderShapes('toFront', ids)
|
const changes = getReorderingShapesChanges(this, 'toFront', ids)
|
||||||
|
if (changes) this.updateShapes(changes)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ it('lists shapes in viewport sorted by id with correct indexes & background inde
|
||||||
])
|
])
|
||||||
|
|
||||||
// Send B to the back
|
// Send B to the back
|
||||||
editor.reorderShapes('toBack', [ids.B])
|
editor.sendToBack([ids.B])
|
||||||
|
|
||||||
// The items should still be sorted by id
|
// The items should still be sorted by id
|
||||||
expect(normalizeIndexes(editor.renderingShapes)).toStrictEqual([
|
expect(normalizeIndexes(editor.renderingShapes)).toStrictEqual([
|
||||||
|
|
|
@ -1,25 +1,918 @@
|
||||||
// import { TestEditor } from '../TestEditor'
|
import { TLShapeId, createShapeId } from '@tldraw/tlschema'
|
||||||
|
import { TestEditor } from '../TestEditor'
|
||||||
|
|
||||||
// let editor: TestEditor
|
let editor: TestEditor
|
||||||
|
|
||||||
// beforeEach(() => {
|
function expectShapesInOrder(editor: TestEditor, ...ids: TLShapeId[]) {
|
||||||
// editor =new TestEditor()
|
expect(editor.sortedShapesArray.map((shape) => shape.id)).toMatchObject(ids)
|
||||||
// })
|
}
|
||||||
|
|
||||||
describe('Send to Back', () => {
|
function getSiblingBelow(editor: TestEditor, id: TLShapeId) {
|
||||||
it.todo('Reorders shapes to the back of the list')
|
const shape = editor.getShapeById(id)!
|
||||||
|
const siblings = editor.getSortedChildIds(shape.parentId)
|
||||||
|
const index = siblings.indexOf(id)
|
||||||
|
return siblings[index - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSiblingAbove(editor: TestEditor, id: TLShapeId) {
|
||||||
|
const shape = editor.getShapeById(id)!
|
||||||
|
const siblings = editor.getSortedChildIds(shape.parentId)
|
||||||
|
const index = siblings.indexOf(id)
|
||||||
|
return siblings[index + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = {
|
||||||
|
A: createShapeId('A'),
|
||||||
|
B: createShapeId('B'),
|
||||||
|
C: createShapeId('C'),
|
||||||
|
D: createShapeId('D'),
|
||||||
|
E: createShapeId('E'),
|
||||||
|
F: createShapeId('F'),
|
||||||
|
G: createShapeId('G'),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
editor?.dispose()
|
||||||
|
editor = new TestEditor()
|
||||||
|
editor.createShapes([
|
||||||
|
{
|
||||||
|
id: ids['A'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids['B'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids['C'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids['D'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids['E'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids['F'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids['G'],
|
||||||
|
type: 'geo',
|
||||||
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Send backward', () => {
|
describe('When running zindex tests', () => {
|
||||||
it.todo('Reorders shapes backward in the list')
|
it('Correctly initializes indices', () => {
|
||||||
|
expect(editor.sortedShapesArray.map((shape) => shape.index)).toMatchObject([
|
||||||
|
'a1',
|
||||||
|
'a2',
|
||||||
|
'a3',
|
||||||
|
'a4',
|
||||||
|
'a5',
|
||||||
|
'a6',
|
||||||
|
'a7',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Correctly identifies shape orders', () => {
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Bring forward', () => {
|
describe('editor.getSiblingAbove', () => {
|
||||||
it.todo('Reorders shapes forward in the list')
|
it('Gets the correct shape above', () => {
|
||||||
|
expect(getSiblingAbove(editor, ids['B'])).toBe(ids['C'])
|
||||||
|
expect(getSiblingAbove(editor, ids['C'])).toBe(ids['D'])
|
||||||
|
expect(getSiblingAbove(editor, ids['G'])).toBeUndefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Bring to Front', () => {
|
describe('editor.getSiblingAbove', () => {
|
||||||
it.todo('Reorders shapes to the front of the list')
|
it('Gets the correct shape above', () => {
|
||||||
|
expect(getSiblingBelow(editor, ids['A'])).toBeUndefined()
|
||||||
|
expect(getSiblingBelow(editor, ids['B'])).toBe(ids['A'])
|
||||||
|
expect(getSiblingBelow(editor, ids['C'])).toBe(ids['B'])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it.todo('Does and undoes')
|
describe('When sending to back', () => {
|
||||||
|
it('Moves one shape to back', () => {
|
||||||
|
editor.sendToBack([ids['D']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['D'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendToBack([ids['D']]) // noop
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['D'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves no shapes when selecting shapes at the back', () => {
|
||||||
|
editor.sendToBack([ids['A'], ids['B'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendToBack([ids['A'], ids['B'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves two adjacent shapes to back', () => {
|
||||||
|
editor.sendToBack([ids['D'], ids['E']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendToBack([ids['D'], ids['E']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes to back', () => {
|
||||||
|
editor.sendToBack([ids['E'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['E'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
editor.sendToBack([ids['E'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['E'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes to back when one is at the back', () => {
|
||||||
|
editor.sendToBack([ids['A'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['G'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
editor.sendToBack([ids['A'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['G'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When sending to front', () => {
|
||||||
|
it('Moves one shape to front', () => {
|
||||||
|
editor.bringToFront([ids['A']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A']
|
||||||
|
)
|
||||||
|
editor.bringToFront([ids['A']]) // noop
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves no shapes when selecting shapes at the front', () => {
|
||||||
|
editor.bringToFront([ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringToFront([ids['G']]) // noop
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves two adjacent shapes to front', () => {
|
||||||
|
editor.bringToFront([ids['D'], ids['E']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
editor.bringToFront([ids['D'], ids['E']]) // noop
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes to front', () => {
|
||||||
|
editor.bringToFront([ids['A'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A'],
|
||||||
|
ids['C']
|
||||||
|
)
|
||||||
|
editor.bringToFront([ids['A'], ids['C']]) // noop
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A'],
|
||||||
|
ids['C']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes to front when one is at the front', () => {
|
||||||
|
editor.bringToFront([ids['E'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['E'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringToFront([ids['E'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['E'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When sending backward', () => {
|
||||||
|
it('Moves one shape backward', () => {
|
||||||
|
editor.sendBackward([ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['C'],
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['C'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves shapes to the first position', () => {
|
||||||
|
editor.sendBackward([ids['B']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['A'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['A']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['B']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['A'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves two shapes to the first position', () => {
|
||||||
|
editor.sendBackward([ids['B'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['A'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['C'], ids['A']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['C'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['A'], ids['B']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves no shapes when sending shapes at the back', () => {
|
||||||
|
editor.sendBackward([ids['A'], ids['B'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['A'], ids['B'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves two adjacent shapes backward', () => {
|
||||||
|
editor.sendBackward([ids['D'], ids['E']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves two adjacent shapes backward when one is at the back', () => {
|
||||||
|
editor.sendBackward([ids['A'], ids['E']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['E'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['A'], ids['E']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['E'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes backward', () => {
|
||||||
|
editor.sendBackward([ids['E'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['E'],
|
||||||
|
ids['D'],
|
||||||
|
ids['G'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['E'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['E'],
|
||||||
|
ids['C'],
|
||||||
|
ids['G'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes backward when one is at the back', () => {
|
||||||
|
editor.sendBackward([ids['A'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['G'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['A'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['G'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes to backward when both are at the back', () => {
|
||||||
|
editor.sendBackward([ids['A'], ids['B']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.sendBackward([ids['A'], ids['B']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When moving forward', () => {
|
||||||
|
it('Moves one shape forward', () => {
|
||||||
|
editor.bringForward([ids['A']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['A'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringForward([ids['A']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['A'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves no shapes when sending shapes at the front', () => {
|
||||||
|
editor.bringForward([ids['E'], ids['F'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringForward([ids['E'], ids['F'], ids['G']]) // noop
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves two adjacent shapes forward', () => {
|
||||||
|
editor.bringForward([ids['C'], ids['D']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['E'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringForward([ids['C'], ids['D']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes forward', () => {
|
||||||
|
editor.bringForward([ids['A'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['A'],
|
||||||
|
ids['D'],
|
||||||
|
ids['C'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringForward([ids['A'], ids['C']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['A'],
|
||||||
|
ids['E'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves non-adjacent shapes to forward when one is at the front', () => {
|
||||||
|
editor.bringForward([ids['C'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['C'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringForward([ids['C'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Moves adjacent shapes to forward when both are at the front', () => {
|
||||||
|
editor.bringForward([ids['F'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
editor.bringForward([ids['F'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Edges
|
||||||
|
|
||||||
|
describe('Edge cases...', () => {
|
||||||
|
it('When bringing forward, does not increment order if shapes at at the top', () => {
|
||||||
|
editor.bringForward([ids['F'], ids['G']])
|
||||||
|
})
|
||||||
|
it('When bringing forward, does not increment order with non-adjacent shapes if shapes at at the top', () => {
|
||||||
|
editor.bringForward([ids['E'], ids['G']])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('When bringing to front, does not change order of shapes already at top', () => {
|
||||||
|
editor.bringToFront([ids['E'], ids['G']])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('When sending to back, does not change order of shapes already at bottom', () => {
|
||||||
|
editor.sendToBack([ids['A'], ids['C']])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('When moving back to front...', () => {
|
||||||
|
editor.sendBackward([ids['F'], ids['G']])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.sendBackward([ids['F'], ids['G']])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.sendBackward([ids['F'], ids['G']])
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.sendBackward([ids['F'], ids['G']])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.sendBackward([ids['F'], ids['G']])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor
|
||||||
|
.bringForward([ids['F'], ids['G']])
|
||||||
|
.bringForward([ids['F'], ids['G']])
|
||||||
|
.bringForward([ids['F'], ids['G']])
|
||||||
|
.bringForward([ids['F'], ids['G']])
|
||||||
|
.bringForward([ids['F'], ids['G']])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When undoing and redoing...', () => {
|
||||||
|
it('Undoes and redoes', () => {
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.mark()
|
||||||
|
editor.sendBackward([ids['F'], ids['G']])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G'],
|
||||||
|
ids['E']
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.undo()
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['B'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
// .redo()
|
||||||
|
// .expectShapesInOrder(ids['A'], ids['B'], ids['C'], ids['D'], ids['F'], ids['G'], ids['E'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When shapes are parented...', () => {
|
||||||
|
it('Sorted correctly by pageIndex', () => {
|
||||||
|
editor.reparentShapesById([ids['C']], ids['A']).reparentShapesById([ids['B']], ids['D'])
|
||||||
|
|
||||||
|
expectShapesInOrder(
|
||||||
|
editor,
|
||||||
|
ids['A'],
|
||||||
|
ids['C'],
|
||||||
|
ids['D'],
|
||||||
|
ids['B'],
|
||||||
|
ids['E'],
|
||||||
|
ids['F'],
|
||||||
|
ids['G']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
233
packages/editor/src/lib/utils/reorderShapes.ts
Normal file
233
packages/editor/src/lib/utils/reorderShapes.ts
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
import { getIndicesBetween, sortByIndex } from '@tldraw/indices'
|
||||||
|
import { TLParentId, TLShape, TLShapeId, TLShapePartial } from '@tldraw/tlschema'
|
||||||
|
import { compact } 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.getShapeById(id)))) {
|
||||||
|
const { parentId } = shape
|
||||||
|
if (!parents.has(parentId)) {
|
||||||
|
parents.set(parentId, {
|
||||||
|
children: compact(editor.getSortedChildIds(parentId).map((id) => editor.getShapeById(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: string | undefined
|
||||||
|
let above: string | 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: string | undefined
|
||||||
|
let above: string | 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue