[improvement] Add getContent
and insertContent
methods (#726)
* Add insertContent method, update copyJson * Add more tests * Update TldrawApp.spec.ts * Adds option object for select, point, and uses in paste
This commit is contained in:
parent
0cfa44f4d2
commit
6d91916804
7 changed files with 1384 additions and 224 deletions
|
@ -4,6 +4,9 @@ import { ArrowShape, ColorStyle, SessionType, TDShapeType } from '~types'
|
|||
import { deepCopy } from './StateManager/copy'
|
||||
import type { SelectTool } from './tools/SelectTool'
|
||||
|
||||
window.focus = jest.fn()
|
||||
global.console.warn = jest.fn()
|
||||
|
||||
describe('TldrawTestApp', () => {
|
||||
describe('When copying and pasting...', () => {
|
||||
it('copies a shape', () => {
|
||||
|
|
|
@ -81,6 +81,7 @@ import { StickyTool } from './tools/StickyTool'
|
|||
import { StateManager } from './StateManager'
|
||||
import { clearPrevSize } from './shapes/shared/getTextSize'
|
||||
import { getClipboard, setClipboard } from './IdbClipboard'
|
||||
import { deepCopy } from './StateManager/copy'
|
||||
|
||||
const uuid = Utils.uniqueId()
|
||||
|
||||
|
@ -1713,43 +1714,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
/* Clipboard */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
private getClipboard(ids = this.selectedIds):
|
||||
| {
|
||||
shapes: TDShape[]
|
||||
bindings: TDBinding[]
|
||||
assets: TDAsset[]
|
||||
}
|
||||
| undefined {
|
||||
const copyingShapeIds = ids.flatMap((id) =>
|
||||
TLDR.getDocumentBranch(this.state, id, this.currentPageId)
|
||||
)
|
||||
|
||||
const copyingShapes = copyingShapeIds.map((id) =>
|
||||
Utils.deepClone(this.getShape(id, this.currentPageId))
|
||||
)
|
||||
|
||||
if (copyingShapes.length === 0) return
|
||||
|
||||
const copyingBindings: TDBinding[] = Object.values(this.page.bindings).filter(
|
||||
(binding) =>
|
||||
copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId)
|
||||
)
|
||||
|
||||
const copyingAssets = copyingShapes
|
||||
.map((shape) => {
|
||||
if (!shape.assetId) return
|
||||
|
||||
return this.document.assets[shape.assetId]
|
||||
})
|
||||
.filter(Boolean) as TDAsset[]
|
||||
|
||||
return {
|
||||
shapes: copyingShapes,
|
||||
bindings: copyingBindings,
|
||||
assets: copyingAssets,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cut (copy and delete) one or more shapes to the clipboard.
|
||||
* @param ids The ids of the shapes to cut.
|
||||
|
@ -1775,7 +1739,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
e?.preventDefault()
|
||||
|
||||
this.clipboard = this.getClipboard(ids)
|
||||
this.clipboard = this.getContent(ids)
|
||||
|
||||
const jsonString = JSON.stringify({
|
||||
type: 'tldr/clipboard',
|
||||
|
@ -1811,97 +1775,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
if (this.readOnly) return
|
||||
|
||||
const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[], assets: TDAsset[]) => {
|
||||
const idsMap: Record<string, string> = {}
|
||||
|
||||
const newAssets = assets.filter((asset) => this.document.assets[asset.id] === undefined)
|
||||
|
||||
if (newAssets.length) {
|
||||
this.patchState({
|
||||
document: {
|
||||
assets: Object.fromEntries(newAssets.map((asset) => [asset.id, asset])),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId()))
|
||||
|
||||
bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
|
||||
|
||||
let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
|
||||
|
||||
const shapesToPaste = shapes
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => {
|
||||
const parentShapeId = idsMap[shape.parentId]
|
||||
|
||||
const copy = {
|
||||
...shape,
|
||||
id: idsMap[shape.id],
|
||||
parentId: parentShapeId || this.currentPageId,
|
||||
}
|
||||
|
||||
if (shape.children) {
|
||||
copy.children = shape.children.map((id) => idsMap[id])
|
||||
}
|
||||
|
||||
if (!parentShapeId) {
|
||||
copy.childIndex = startIndex
|
||||
startIndex++
|
||||
}
|
||||
|
||||
if (copy.handles) {
|
||||
Object.values(copy.handles).forEach((handle) => {
|
||||
if (handle.bindingId) {
|
||||
handle.bindingId = idsMap[handle.bindingId]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return copy
|
||||
})
|
||||
|
||||
const bindingsToPaste = bindings.map((binding) => ({
|
||||
...binding,
|
||||
id: idsMap[binding.id],
|
||||
toId: idsMap[binding.toId],
|
||||
fromId: idsMap[binding.fromId],
|
||||
}))
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
||||
|
||||
let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
|
||||
|
||||
if (
|
||||
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
||||
Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
|
||||
) {
|
||||
center = Vec.add(center, this.pasteInfo.offset)
|
||||
this.pasteInfo.offset = Vec.add(this.pasteInfo.offset, [GRID_SIZE, GRID_SIZE])
|
||||
} else {
|
||||
this.pasteInfo.center = center
|
||||
this.pasteInfo.offset = [0, 0]
|
||||
}
|
||||
|
||||
const centeredBounds = Utils.centerBounds(commonBounds, center)
|
||||
|
||||
const delta = Vec.sub(
|
||||
Utils.getBoundsCenter(centeredBounds),
|
||||
Utils.getBoundsCenter(commonBounds)
|
||||
)
|
||||
|
||||
this.create(
|
||||
shapesToPaste.map((shape) =>
|
||||
TLDR.getShapeUtil(shape.type).create({
|
||||
...shape,
|
||||
point: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
parentId: shape.parentId || this.currentPageId,
|
||||
})
|
||||
),
|
||||
bindingsToPaste
|
||||
)
|
||||
}
|
||||
|
||||
const pasteTextAsSvg = async (text: string) => {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = text
|
||||
|
@ -1951,7 +1824,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
} = JSON.parse(maybeJson)
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets)
|
||||
this.insertContent(json, { point, select: true })
|
||||
return
|
||||
} else {
|
||||
throw Error('Not tldraw data!')
|
||||
|
@ -2058,7 +1931,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
} else {
|
||||
TLDR.warn('This browser does not support the Clipboard API!')
|
||||
if (this.clipboard) {
|
||||
pasteInCurrentPage(this.clipboard.shapes, this.clipboard.bindings, this.clipboard.assets)
|
||||
this.insertContent(this.clipboard, { point, select: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2076,7 +1949,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
||||
|
||||
window.focus() // weird but necessary
|
||||
if (typeof window !== 'undefined') {
|
||||
window.focus() // weird but necessary
|
||||
}
|
||||
|
||||
if (opts.includeFonts) {
|
||||
try {
|
||||
|
@ -2238,7 +2113,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
const svgString = TLDR.getSvgString(svg, 1)
|
||||
|
||||
this.clipboard = this.getClipboard(ids)
|
||||
this.clipboard = this.getContent(ids)
|
||||
|
||||
const tldrawString = JSON.stringify({
|
||||
type: 'tldr/clipboard',
|
||||
|
@ -2257,23 +2132,104 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
return svgString
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shapes and bindings for the current selection, if any, or else the current page.
|
||||
*
|
||||
* @param ids The ids of the shapes to get content for.
|
||||
*/
|
||||
getContent = (ids?: string[]) => {
|
||||
const page = this.getPage(this.currentPageId)
|
||||
|
||||
// If ids is explicitly empty ([]) return
|
||||
if (ids && ids.length === 0) return
|
||||
|
||||
// If ids was not provided, use the selected ids
|
||||
if (!ids) ids = this.selectedIds
|
||||
|
||||
// If there are no selected ids, use all the page's shape ids
|
||||
if (ids.length === 0) ids = Object.keys(page.shapes)
|
||||
|
||||
// If the page was empty, return
|
||||
if (ids.length === 0) return
|
||||
|
||||
const shapes = ids
|
||||
.map((id) => page.shapes[id])
|
||||
.flatMap((shape) => [shape, ...(shape.children ?? []).map((childId) => page.shapes[childId])])
|
||||
.map(deepCopy)
|
||||
|
||||
const idsSet = new Set(shapes.map((s) => s.id))
|
||||
|
||||
shapes.forEach((shape) => {
|
||||
if (shape.parentId === this.currentPageId) {
|
||||
shape.parentId = 'currentPageId'
|
||||
}
|
||||
})
|
||||
|
||||
// If a binding's from and to are included, then include the binding;
|
||||
// but if only one shape is included, discard the binding
|
||||
const bindings = Object.values(page.bindings)
|
||||
.filter((binding) => {
|
||||
if (idsSet.has(binding.fromId) && idsSet.has(binding.toId)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (idsSet.has(binding.fromId)) {
|
||||
const shape = shapes.find((s) => s.id === binding.fromId)
|
||||
const handles = shape!.handles
|
||||
if (handles) {
|
||||
Object.values(handles).forEach((handle) => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (idsSet.has(binding.toId)) {
|
||||
const shape = shapes.find((s) => s.id === binding.toId)
|
||||
const handles = shape!.handles
|
||||
if (handles) {
|
||||
Object.values(handles).forEach((handle) => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
.map(deepCopy)
|
||||
|
||||
const assets = [
|
||||
...new Set(
|
||||
shapes
|
||||
.map((shape) => {
|
||||
if (!shape.assetId) return
|
||||
return this.document.assets[shape.assetId]
|
||||
})
|
||||
.filter(Boolean)
|
||||
.map(deepCopy)
|
||||
),
|
||||
] as TDAsset[]
|
||||
|
||||
return { shapes, bindings, assets }
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy one or more shapes as JSON.
|
||||
* @param ids The ids of the shapes to copy.
|
||||
* @param pageId The page from which to copy the shapes.
|
||||
* @returns A string containing the JSON.
|
||||
*/
|
||||
copyJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
||||
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
||||
copyJson = (ids = this.selectedIds) => {
|
||||
const content = this.getContent(ids)
|
||||
|
||||
if (ids.length === 0) return
|
||||
if (content) {
|
||||
TLDR.copyStringToClipboard(JSON.stringify(content))
|
||||
}
|
||||
|
||||
const shapes = ids.map((id) => this.getShape(id, pageId))
|
||||
const json = JSON.stringify(shapes, null, 2)
|
||||
|
||||
TLDR.copyStringToClipboard(json)
|
||||
|
||||
return json
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2281,20 +2237,37 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* @param ids The ids of the shapes to copy from the current page.
|
||||
* @returns A string containing the JSON.
|
||||
*/
|
||||
exportJson = (
|
||||
ids = this.selectedIds.length ? this.selectedIds : Object.keys(this.page.shapes)
|
||||
exportJson = (ids = this.selectedIds) => {
|
||||
const content = this.getContent(ids)
|
||||
|
||||
if (content) {
|
||||
const blob = new Blob([JSON.stringify(content)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `export.json`
|
||||
link.click()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert content.
|
||||
*
|
||||
* @param content The content to insert.
|
||||
* @param content.shapes An array of TDShape objects.
|
||||
* @param content.bindings (optional) An array of TDBinding objects.
|
||||
* @param content.assets (optional) An array of TDAsset objects.
|
||||
* @param opts (optional) An options object
|
||||
* @param opts.point (optional) A point at which to paste the content.
|
||||
* @param opts.select (optional) When true, the inserted shapes will be selected.
|
||||
*/
|
||||
insertContent = (
|
||||
content: { shapes: TDShape[]; bindings?: TDBinding[]; assets?: TDAsset[] },
|
||||
opts = {} as { point?: number[]; select?: boolean }
|
||||
) => {
|
||||
if (ids.length === 0) return
|
||||
|
||||
const shapes = ids.map((id) => this.getShape(id, this.currentPageId))
|
||||
const json = JSON.stringify(shapes, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `export.json`
|
||||
link.click()
|
||||
return this.setState(Commands.insertContent(this, content, opts), 'insert_content')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,83 +1,827 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[` 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"id\\": \\"rect1\\",
|
||||
\\"parentId\\": \\"page1\\",
|
||||
\\"name\\": \\"Rectangle\\",
|
||||
\\"childIndex\\": 1,
|
||||
\\"type\\": \\"rectangle\\",
|
||||
\\"point\\": [
|
||||
TldrawTestApp {
|
||||
"_idbId": undefined,
|
||||
"_snapshot": Object {
|
||||
"appState": Object {
|
||||
"activeTool": "select",
|
||||
"currentPageId": "page",
|
||||
"currentStyle": Object {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
"isFilled": false,
|
||||
"scale": 1,
|
||||
"size": "small",
|
||||
},
|
||||
"disableAssets": false,
|
||||
"eraseLine": Array [],
|
||||
"hoveredId": undefined,
|
||||
"isEmptyCanvas": false,
|
||||
"isLoading": false,
|
||||
"isMenuOpen": false,
|
||||
"isToolLocked": false,
|
||||
"snapLines": Array [],
|
||||
"status": "idle",
|
||||
},
|
||||
"document": Object {
|
||||
"assets": Object {},
|
||||
"id": "doc",
|
||||
"name": "New Document",
|
||||
"pageStates": Object {
|
||||
"page": Object {
|
||||
"camera": Object {
|
||||
"point": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"zoom": 1,
|
||||
},
|
||||
"id": "page",
|
||||
"selectedIds": Array [],
|
||||
},
|
||||
},
|
||||
"pages": Object {
|
||||
"page": Object {
|
||||
"bindings": Object {},
|
||||
"childIndex": 1,
|
||||
"id": "page",
|
||||
"name": "Page 1",
|
||||
"shapes": Object {},
|
||||
},
|
||||
},
|
||||
"version": 15.3,
|
||||
},
|
||||
"settings": Object {
|
||||
"isCadSelectMode": false,
|
||||
"isDarkMode": false,
|
||||
"isDebugMode": false,
|
||||
"isFocusMode": false,
|
||||
"isPenMode": false,
|
||||
"isReadonlyMode": false,
|
||||
"isSnapping": false,
|
||||
"isZoomSnap": false,
|
||||
"keepStyleMenuOpen": false,
|
||||
"language": "en",
|
||||
"nudgeDistanceLarge": 16,
|
||||
"nudgeDistanceSmall": 1,
|
||||
"showBindingHandles": true,
|
||||
"showCloneHandles": false,
|
||||
"showGrid": false,
|
||||
"showRotateHandles": true,
|
||||
},
|
||||
},
|
||||
"_state": Object {
|
||||
"appState": Object {
|
||||
"activeTool": "select",
|
||||
"currentPageId": "page1",
|
||||
"currentStyle": Object {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
"isFilled": false,
|
||||
"scale": 1,
|
||||
"size": "small",
|
||||
},
|
||||
"disableAssets": false,
|
||||
"eraseLine": Array [],
|
||||
"hoveredId": undefined,
|
||||
"isEmptyCanvas": false,
|
||||
"isLoading": false,
|
||||
"isMenuOpen": false,
|
||||
"isToolLocked": false,
|
||||
"snapLines": Array [],
|
||||
"status": "idle",
|
||||
},
|
||||
"document": Object {
|
||||
"assets": Object {},
|
||||
"id": "doc",
|
||||
"name": "New Document",
|
||||
"pageStates": Object {
|
||||
"page1": Object {
|
||||
"bindingId": undefined,
|
||||
"camera": Object {
|
||||
"point": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"zoom": 1,
|
||||
},
|
||||
"editingId": undefined,
|
||||
"hoveredId": undefined,
|
||||
"id": "page1",
|
||||
"pointedId": undefined,
|
||||
"selectedIds": Array [
|
||||
"rect1",
|
||||
"rect2",
|
||||
"rect3",
|
||||
],
|
||||
},
|
||||
},
|
||||
"pages": Object {
|
||||
"page1": Object {
|
||||
"bindings": Object {},
|
||||
"id": "page1",
|
||||
"shapes": Object {
|
||||
"rect1": Object {
|
||||
"childIndex": 1,
|
||||
"id": "rect1",
|
||||
"label": "",
|
||||
"labelPoint": Array [
|
||||
0.5,
|
||||
0.5,
|
||||
],
|
||||
"name": "Rectangle",
|
||||
"parentId": "page1",
|
||||
"point": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"size": Array [
|
||||
100,
|
||||
100,
|
||||
],
|
||||
"style": Object {
|
||||
"color": "blue",
|
||||
"dash": "draw",
|
||||
"size": "medium",
|
||||
},
|
||||
"type": "rectangle",
|
||||
},
|
||||
"rect2": Object {
|
||||
"childIndex": 2,
|
||||
"id": "rect2",
|
||||
"label": "",
|
||||
"labelPoint": Array [
|
||||
0.5,
|
||||
0.5,
|
||||
],
|
||||
"name": "Rectangle",
|
||||
"parentId": "page1",
|
||||
"point": Array [
|
||||
100,
|
||||
100,
|
||||
],
|
||||
"size": Array [
|
||||
100,
|
||||
100,
|
||||
],
|
||||
"style": Object {
|
||||
"color": "blue",
|
||||
"dash": "draw",
|
||||
"size": "medium",
|
||||
},
|
||||
"type": "rectangle",
|
||||
},
|
||||
"rect3": Object {
|
||||
"childIndex": 3,
|
||||
"id": "rect3",
|
||||
"label": "",
|
||||
"labelPoint": Array [
|
||||
0.5,
|
||||
0.5,
|
||||
],
|
||||
"name": "Rectangle",
|
||||
"parentId": "page1",
|
||||
"point": Array [
|
||||
20,
|
||||
20,
|
||||
],
|
||||
"size": Array [
|
||||
100,
|
||||
100,
|
||||
],
|
||||
"style": Object {
|
||||
"color": "blue",
|
||||
"dash": "draw",
|
||||
"size": "medium",
|
||||
},
|
||||
"type": "rectangle",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"version": 15.3,
|
||||
},
|
||||
"settings": Object {
|
||||
"isCadSelectMode": false,
|
||||
"isDarkMode": false,
|
||||
"isDebugMode": false,
|
||||
"isFocusMode": false,
|
||||
"isPenMode": false,
|
||||
"isReadonlyMode": false,
|
||||
"isSnapping": false,
|
||||
"isZoomSnap": false,
|
||||
"keepStyleMenuOpen": false,
|
||||
"language": "en",
|
||||
"nudgeDistanceLarge": 16,
|
||||
"nudgeDistanceSmall": 1,
|
||||
"showBindingHandles": true,
|
||||
"showCloneHandles": false,
|
||||
"showGrid": false,
|
||||
"showRotateHandles": true,
|
||||
},
|
||||
},
|
||||
"_status": "ready",
|
||||
"addMediaFromFile": [Function],
|
||||
"addToSelectHistory": [Function],
|
||||
"align": [Function],
|
||||
"altKey": false,
|
||||
"applyPatch": [Function],
|
||||
"broadcastPageChanges": [Function],
|
||||
"callbacks": Object {},
|
||||
"cancel": [Function],
|
||||
"cancelSession": [Function],
|
||||
"changePage": [Function],
|
||||
"cleanup": [Function],
|
||||
"clearSelectHistory": [Function],
|
||||
"clickBounds": [Function],
|
||||
"clickBoundsHandle": [Function],
|
||||
"clickCanvas": [Function],
|
||||
"clickShape": [Function],
|
||||
"completeSession": [Function],
|
||||
"copy": [Function],
|
||||
"copyImage": [Function],
|
||||
"copyJson": [Function],
|
||||
"copySvg": [Function],
|
||||
"create": [Function],
|
||||
"createPage": [Function],
|
||||
"createShapes": [Function],
|
||||
"ctrlKey": false,
|
||||
"currentPoint": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"currentTool": SelectTool {
|
||||
"app": [Circular],
|
||||
"clonePaint": [Function],
|
||||
"getNextChildIndex": [Function],
|
||||
"getShapeClone": [Function],
|
||||
"onCancel": [Function],
|
||||
"onDoubleClickBoundsHandle": [Function],
|
||||
"onDoubleClickCanvas": [Function],
|
||||
"onDoubleClickHandle": [Function],
|
||||
"onDoubleClickShape": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onHoverShape": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointBounds": [Function],
|
||||
"onPointBoundsHandle": [Function],
|
||||
"onPointHandle": [Function],
|
||||
"onPointShape": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"onReleaseBounds": [Function],
|
||||
"onReleaseBoundsHandle": [Function],
|
||||
"onReleaseHandle": [Function],
|
||||
"onRightPointBounds": [Function],
|
||||
"onRightPointShape": [Function],
|
||||
"onShapeClone": [Function],
|
||||
"onUnhoverShape": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "select",
|
||||
},
|
||||
"cut": [Function],
|
||||
"delete": [Function],
|
||||
"deleteAll": [Function],
|
||||
"deletePage": [Function],
|
||||
"distribute": [Function],
|
||||
"doubleClickBoundHandle": [Function],
|
||||
"doubleClickShape": [Function],
|
||||
"duplicate": [Function],
|
||||
"duplicatePage": [Function],
|
||||
"editingStartTime": -1,
|
||||
"expectSelectedIdsToBe": [Function],
|
||||
"expectShapesToBeAtPoints": [Function],
|
||||
"expectShapesToHaveProps": [Function],
|
||||
"exportImage": [Function],
|
||||
"exportJson": [Function],
|
||||
"fileSystemHandle": null,
|
||||
"flipHorizontal": [Function],
|
||||
"flipVertical": [Function],
|
||||
"forceUpdate": [Function],
|
||||
"getAppState": [Function],
|
||||
"getBinding": [Function],
|
||||
"getBindings": [Function],
|
||||
"getContent": [Function],
|
||||
"getImage": [Function],
|
||||
"getPage": [Function],
|
||||
"getPagePoint": [Function],
|
||||
"getPageState": [Function],
|
||||
"getReservedContent": [Function],
|
||||
"getShape": [Function],
|
||||
"getShapeBounds": [Function],
|
||||
"getShapeUtil": [Function],
|
||||
"getShapes": [Function],
|
||||
"getSvg": [Function],
|
||||
"getViewboxFromSVG": [Function],
|
||||
"group": [Function],
|
||||
"hoverShape": [Function],
|
||||
"initialState": Object {
|
||||
"appState": Object {
|
||||
"activeTool": "select",
|
||||
"currentPageId": "page",
|
||||
"currentStyle": Object {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
"isFilled": false,
|
||||
"scale": 1,
|
||||
"size": "small",
|
||||
},
|
||||
"disableAssets": false,
|
||||
"eraseLine": Array [],
|
||||
"hoveredId": undefined,
|
||||
"isEmptyCanvas": false,
|
||||
"isLoading": false,
|
||||
"isMenuOpen": false,
|
||||
"isToolLocked": false,
|
||||
"snapLines": Array [],
|
||||
"status": "idle",
|
||||
},
|
||||
"document": Object {
|
||||
"assets": Object {},
|
||||
"id": "doc",
|
||||
"name": "New Document",
|
||||
"pageStates": Object {
|
||||
"page": Object {
|
||||
"camera": Object {
|
||||
"point": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"zoom": 1,
|
||||
},
|
||||
"id": "page",
|
||||
"selectedIds": Array [],
|
||||
},
|
||||
},
|
||||
"pages": Object {
|
||||
"page": Object {
|
||||
"bindings": Object {},
|
||||
"childIndex": 1,
|
||||
"id": "page",
|
||||
"name": "Page 1",
|
||||
"shapes": Object {},
|
||||
},
|
||||
},
|
||||
"version": 15.3,
|
||||
},
|
||||
"settings": Object {
|
||||
"isCadSelectMode": false,
|
||||
"isDarkMode": false,
|
||||
"isDebugMode": false,
|
||||
"isFocusMode": false,
|
||||
"isPenMode": false,
|
||||
"isReadonlyMode": false,
|
||||
"isSnapping": false,
|
||||
"isZoomSnap": false,
|
||||
"keepStyleMenuOpen": false,
|
||||
"language": "en",
|
||||
"nudgeDistanceLarge": 16,
|
||||
"nudgeDistanceSmall": 1,
|
||||
"showBindingHandles": true,
|
||||
"showCloneHandles": false,
|
||||
"showGrid": false,
|
||||
"showRotateHandles": true,
|
||||
},
|
||||
},
|
||||
"insertContent": [Function],
|
||||
"isCreating": false,
|
||||
"isDirty": false,
|
||||
"isForcePanning": false,
|
||||
"isPaused": false,
|
||||
"isPointing": false,
|
||||
"justSent": false,
|
||||
"loadDocument": [Function],
|
||||
"loadRoom": [Function],
|
||||
"mergeDocument": [Function],
|
||||
"metaKey": false,
|
||||
"migrate": [Function],
|
||||
"moveBackward": [Function],
|
||||
"moveForward": [Function],
|
||||
"movePointer": [Function],
|
||||
"moveToBack": [Function],
|
||||
"moveToFront": [Function],
|
||||
"moveToPage": [Function],
|
||||
"newProject": [Function],
|
||||
"nudge": [Function],
|
||||
"onCommand": [Function],
|
||||
"onDoubleClickBounds": [Function],
|
||||
"onDoubleClickBoundsHandle": [Function],
|
||||
"onDoubleClickCanvas": [Function],
|
||||
"onDoubleClickHandle": [Function],
|
||||
"onDoubleClickShape": [Function],
|
||||
"onDragBounds": [Function],
|
||||
"onDragBoundsHandle": [Function],
|
||||
"onDragCanvas": [Function],
|
||||
"onDragHandle": [Function],
|
||||
"onDragOver": [Function],
|
||||
"onDragShape": [Function],
|
||||
"onDrop": [Function],
|
||||
"onError": [Function],
|
||||
"onHoverBounds": [Function],
|
||||
"onHoverBoundsHandle": [Function],
|
||||
"onHoverHandle": [Function],
|
||||
"onHoverShape": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPan": [Function],
|
||||
"onPatch": [Function],
|
||||
"onPersist": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointBounds": [Function],
|
||||
"onPointBoundsHandle": [Function],
|
||||
"onPointCanvas": [Function],
|
||||
"onPointHandle": [Function],
|
||||
"onPointShape": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"onReady": [Function],
|
||||
"onRedo": [Function],
|
||||
"onReleaseBounds": [Function],
|
||||
"onReleaseBoundsHandle": [Function],
|
||||
"onReleaseCanvas": [Function],
|
||||
"onReleaseHandle": [Function],
|
||||
"onReleaseShape": [Function],
|
||||
"onRenderCountChange": [Function],
|
||||
"onReplace": [Function],
|
||||
"onRightPointBounds": [Function],
|
||||
"onRightPointBoundsHandle": [Function],
|
||||
"onRightPointCanvas": [Function],
|
||||
"onRightPointHandle": [Function],
|
||||
"onRightPointShape": [Function],
|
||||
"onShapeBlur": [Function],
|
||||
"onShapeChange": [Function],
|
||||
"onShapeClone": [Function],
|
||||
"onStateDidChange": [Function],
|
||||
"onUndo": [Function],
|
||||
"onUnhoverBounds": [Function],
|
||||
"onUnhoverBoundsHandle": [Function],
|
||||
"onUnhoverHandle": [Function],
|
||||
"onUnhoverShape": [Function],
|
||||
"onZoom": [Function],
|
||||
"openAsset": [Function],
|
||||
"openProject": [Function],
|
||||
"originPoint": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"pan": [Function],
|
||||
"paste": [Function],
|
||||
"pasteInfo": Object {
|
||||
"center": Array [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
\\"size\\": [
|
||||
100,
|
||||
100
|
||||
"offset": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
\\"style\\": {
|
||||
\\"dash\\": \\"draw\\",
|
||||
\\"size\\": \\"medium\\",
|
||||
\\"color\\": \\"blue\\"
|
||||
},
|
||||
\\"label\\": \\"\\",
|
||||
\\"labelPoint\\": [
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"rect2\\",
|
||||
\\"parentId\\": \\"page1\\",
|
||||
\\"name\\": \\"Rectangle\\",
|
||||
\\"childIndex\\": 2,
|
||||
\\"type\\": \\"rectangle\\",
|
||||
\\"point\\": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
\\"size\\": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
\\"style\\": {
|
||||
\\"dash\\": \\"draw\\",
|
||||
\\"size\\": \\"medium\\",
|
||||
\\"color\\": \\"blue\\"
|
||||
},
|
||||
\\"label\\": \\"\\",
|
||||
\\"labelPoint\\": [
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
"patchCreate": [Function],
|
||||
"patchState": [Function],
|
||||
"persist": [Function],
|
||||
"pinchZoom": [Function],
|
||||
"pointBounds": [Function],
|
||||
"pointBoundsHandle": [Function],
|
||||
"pointCanvas": [Function],
|
||||
"pointShape": [Function],
|
||||
"pointer": -1,
|
||||
"pressKey": [Function],
|
||||
"prevAssets": Object {},
|
||||
"prevBindings": Object {},
|
||||
"prevSelectedIds": Array [],
|
||||
"prevShapes": Object {},
|
||||
"previousPoint": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"readOnly": false,
|
||||
"ready": Promise {},
|
||||
"redo": [Function],
|
||||
"redoSelect": [Function],
|
||||
"refreshBoundingBoxes": [Function],
|
||||
"releaseKey": [Function],
|
||||
"removeUser": [Function],
|
||||
"renamePage": [Function],
|
||||
"rendererBounds": Object {
|
||||
"height": 100,
|
||||
"maxX": 100,
|
||||
"maxY": 100,
|
||||
"minX": 0,
|
||||
"minY": 0,
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"rect3\\",
|
||||
\\"parentId\\": \\"page1\\",
|
||||
\\"name\\": \\"Rectangle\\",
|
||||
\\"childIndex\\": 3,
|
||||
\\"type\\": \\"rectangle\\",
|
||||
\\"point\\": [
|
||||
20,
|
||||
20
|
||||
"replaceHistory": [Function],
|
||||
"replacePageContent": [Function],
|
||||
"replaceState": [Function],
|
||||
"reset": [Function],
|
||||
"resetBounds": [Function],
|
||||
"resetCamera": [Function],
|
||||
"resetDocument": [Function],
|
||||
"resetHistory": [Function],
|
||||
"resetZoom": [Function],
|
||||
"rotate": [Function],
|
||||
"rotationInfo": Object {
|
||||
"center": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
\\"size\\": [
|
||||
100,
|
||||
100
|
||||
"selectedIds": Array [],
|
||||
},
|
||||
"saveProject": [Function],
|
||||
"saveProjectAs": [Function],
|
||||
"select": [Function],
|
||||
"selectAll": [Function],
|
||||
"selectHistory": Object {
|
||||
"pointer": 1,
|
||||
"stack": Array [
|
||||
Array [],
|
||||
Array [
|
||||
"rect1",
|
||||
"rect2",
|
||||
"rect3",
|
||||
],
|
||||
],
|
||||
\\"style\\": {
|
||||
\\"dash\\": \\"draw\\",
|
||||
\\"size\\": \\"medium\\",
|
||||
\\"color\\": \\"blue\\"
|
||||
},
|
||||
"selectNone": [Function],
|
||||
"selectTool": [Function],
|
||||
"session": undefined,
|
||||
"setCamera": [Function],
|
||||
"setDisableAssets": [Function],
|
||||
"setEditingId": [Function],
|
||||
"setHoveredId": [Function],
|
||||
"setIsLoading": [Function],
|
||||
"setMenuOpen": [Function],
|
||||
"setSelectedIds": [Function],
|
||||
"setSetting": [Function],
|
||||
"setShapeProps": [Function],
|
||||
"setSnapshot": [Function],
|
||||
"setState": [Function],
|
||||
"shiftKey": false,
|
||||
"signOut": [Function],
|
||||
"spaceKey": false,
|
||||
"stack": Array [],
|
||||
"startSession": [Function],
|
||||
"stopPointing": [Function],
|
||||
"store": Object {
|
||||
"destroy": [Function],
|
||||
"getState": [Function],
|
||||
"setState": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
"stretch": [Function],
|
||||
"style": [Function],
|
||||
"toggleAspectRatioLocked": [Function],
|
||||
"toggleDarkMode": [Function],
|
||||
"toggleDebugMode": [Function],
|
||||
"toggleDecoration": [Function],
|
||||
"toggleFocusMode": [Function],
|
||||
"toggleGrid": [Function],
|
||||
"toggleHidden": [Function],
|
||||
"toggleLocked": [Function],
|
||||
"togglePenMode": [Function],
|
||||
"toggleToolLock": [Function],
|
||||
"toggleZoomSnap": [Function],
|
||||
"tools": Object {
|
||||
"arrow": ArrowTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "arrow",
|
||||
},
|
||||
\\"label\\": \\"\\",
|
||||
\\"labelPoint\\": [
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
]"
|
||||
"draw": DrawTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "draw",
|
||||
},
|
||||
"ellipse": EllipseTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "ellipse",
|
||||
},
|
||||
"erase": EraseTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "erase",
|
||||
},
|
||||
"line": LineTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "line",
|
||||
},
|
||||
"rectangle": RectangleTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "rectangle",
|
||||
},
|
||||
"select": SelectTool {
|
||||
"app": [Circular],
|
||||
"clonePaint": [Function],
|
||||
"getNextChildIndex": [Function],
|
||||
"getShapeClone": [Function],
|
||||
"onCancel": [Function],
|
||||
"onDoubleClickBoundsHandle": [Function],
|
||||
"onDoubleClickCanvas": [Function],
|
||||
"onDoubleClickHandle": [Function],
|
||||
"onDoubleClickShape": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onHoverShape": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointBounds": [Function],
|
||||
"onPointBoundsHandle": [Function],
|
||||
"onPointHandle": [Function],
|
||||
"onPointShape": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"onReleaseBounds": [Function],
|
||||
"onReleaseBoundsHandle": [Function],
|
||||
"onReleaseHandle": [Function],
|
||||
"onRightPointBounds": [Function],
|
||||
"onRightPointShape": [Function],
|
||||
"onShapeClone": [Function],
|
||||
"onUnhoverShape": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "select",
|
||||
},
|
||||
"sticky": StickyTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "sticky",
|
||||
},
|
||||
"text": TextTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointShape": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"onShapeBlur": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"stopEditingShape": [Function],
|
||||
"type": "text",
|
||||
},
|
||||
"triangle": TriangleTool {
|
||||
"app": [Circular],
|
||||
"getNextChildIndex": [Function],
|
||||
"onCancel": [Function],
|
||||
"onEnter": [Function],
|
||||
"onExit": [Function],
|
||||
"onKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"onPinch": [Function],
|
||||
"onPinchEnd": [Function],
|
||||
"onPinchStart": [Function],
|
||||
"onPointerDown": [Function],
|
||||
"onPointerMove": [Function],
|
||||
"onPointerUp": [Function],
|
||||
"setStatus": [Function],
|
||||
"status": "idle",
|
||||
"type": "triangle",
|
||||
},
|
||||
},
|
||||
"undo": [Function],
|
||||
"undoSelect": [Function],
|
||||
"ungroup": [Function],
|
||||
"updateBounds": [Function],
|
||||
"updateDocument": [Function],
|
||||
"updateInputs": [Function],
|
||||
"updateSession": [Function],
|
||||
"updateShapes": [Function],
|
||||
"updateUsers": [Function],
|
||||
"updateViewport": [Function],
|
||||
"useStore": [Function],
|
||||
"viewport": Object {
|
||||
"height": 100,
|
||||
"maxX": 100,
|
||||
"maxY": 100,
|
||||
"minX": 0,
|
||||
"minY": 0,
|
||||
"width": 100,
|
||||
},
|
||||
"zoomBy": [Function],
|
||||
"zoomIn": [Function],
|
||||
"zoomOut": [Function],
|
||||
"zoomTo": [Function],
|
||||
"zoomToContent": [Function],
|
||||
"zoomToFit": [Function],
|
||||
"zoomToSelection": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`TldrawTestApp Exposes undo/redo stack: history 1`] = `
|
||||
|
|
|
@ -22,3 +22,4 @@ export * from './translateShapes'
|
|||
export * from './ungroupShapes'
|
||||
export * from './updateShapes'
|
||||
export * from './setShapesProps'
|
||||
export * from './insertContent'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './insertContent'
|
|
@ -0,0 +1,187 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
import { ColorStyle, DashStyle, SessionType, SizeStyle, TDShapeType } from '~types'
|
||||
|
||||
let app: TldrawTestApp
|
||||
|
||||
beforeEach(() => {
|
||||
app = new TldrawTestApp()
|
||||
app.loadDocument(mockDocument)
|
||||
})
|
||||
|
||||
describe('insert command', () => {
|
||||
it('Inserts shapes, bindings, etc. into the current page', () => {
|
||||
const content = app.getContent()!
|
||||
const size = app.shapes.length
|
||||
app.insertContent(content)
|
||||
expect(app.shapes.length).toBe(size * 2)
|
||||
})
|
||||
|
||||
it('Selects content when opts.select is true', () => {
|
||||
const content = app.getContent()!
|
||||
const size = app.shapes.length
|
||||
const prevSelectedIds = [...app.selectedIds]
|
||||
app.insertContent(content, { select: true })
|
||||
expect(app.shapes.length).toBe(size * 2)
|
||||
expect(app.selectedIds).not.toMatchObject(prevSelectedIds)
|
||||
})
|
||||
|
||||
it('Centers inserted content at a point', () => {
|
||||
app.select('rect1')
|
||||
const content = app.getContent()!
|
||||
|
||||
const before = [...app.shapes]
|
||||
|
||||
const point = [222, 444]
|
||||
|
||||
app.insertContent(content, { point })
|
||||
|
||||
const inserted = [...app.shapes].filter((s) => !before.includes(s))
|
||||
|
||||
expect(
|
||||
Utils.getBoundsCenter(Utils.getCommonBounds(inserted.map(TLDR.getBounds)))
|
||||
).toMatchObject(point)
|
||||
})
|
||||
|
||||
it('does nothing when ids are explicitly empty', () => {
|
||||
const content = app.getContent([])
|
||||
expect(content).toBe(undefined)
|
||||
})
|
||||
|
||||
it('uses the selected ids when no ids provided', () => {
|
||||
app.select('rect1')
|
||||
const content = app.getContent()!
|
||||
const size = app.shapes.length
|
||||
app.insertContent(content)
|
||||
expect(app.shapes.length).toBe(size + 1)
|
||||
})
|
||||
|
||||
it('uses all shape ids from the page when no selection, either', () => {
|
||||
app.selectNone()
|
||||
const content = app.getContent()!
|
||||
const size = app.shapes.length
|
||||
app.insertContent(content)
|
||||
expect(app.shapes.length).toBe(size * 2)
|
||||
})
|
||||
|
||||
it('does nothing if the page has no shapes, either', () => {
|
||||
app.deleteAll()
|
||||
const content = app.getContent()
|
||||
expect(content).toBe(undefined)
|
||||
})
|
||||
|
||||
it('includes bindings', () => {
|
||||
app
|
||||
.createShapes({
|
||||
type: TDShapeType.Arrow,
|
||||
id: 'arrow1',
|
||||
point: [200, 200],
|
||||
})
|
||||
.select('arrow1')
|
||||
.movePointer([200, 200])
|
||||
.startSession(SessionType.Arrow, 'arrow1', 'start')
|
||||
.movePointer([50, 50])
|
||||
.completeSession()
|
||||
.selectNone()
|
||||
|
||||
expect(app.bindings.length).toBe(1)
|
||||
|
||||
const content = app.getContent()!
|
||||
const size = app.shapes.length
|
||||
|
||||
app.insertContent(content)
|
||||
expect(app.bindings.length).toBe(2)
|
||||
expect(app.shapes.length).toBe(size * 2)
|
||||
})
|
||||
|
||||
it('removes bindings when only one shape is inserted', () => {
|
||||
app
|
||||
.createShapes({
|
||||
type: TDShapeType.Arrow,
|
||||
id: 'arrow1',
|
||||
point: [200, 200],
|
||||
})
|
||||
.select('arrow1')
|
||||
.movePointer([200, 200])
|
||||
.startSession(SessionType.Arrow, 'arrow1', 'start')
|
||||
.movePointer([50, 50])
|
||||
.completeSession()
|
||||
|
||||
expect(app.bindings.length).toBe(1) // arrow1 -> rect3
|
||||
|
||||
app.select('rect3') // select only rect3, not arrow1
|
||||
|
||||
const content = app.getContent()!
|
||||
|
||||
// getContent does not include the incomplete binding
|
||||
expect(Object.values(content.bindings).length).toBe(0)
|
||||
|
||||
app.insertContent(content)
|
||||
|
||||
// insertContent does not paste in the discarded binding
|
||||
expect(app.bindings.length).toBe(1)
|
||||
})
|
||||
|
||||
it('works with groups', () => {
|
||||
app.select('rect1', 'rect2').group().selectAll()
|
||||
|
||||
const content = app.getContent()!
|
||||
|
||||
const size = app.shapes.length
|
||||
|
||||
app.insertContent(content)
|
||||
|
||||
expect(app.shapes.length).toBe(size * 2)
|
||||
})
|
||||
|
||||
it('if a shapes parent is not inserted, inserts to the page instead', () => {
|
||||
app.select('rect1', 'rect2').group().select('rect1')
|
||||
|
||||
const content = app.getContent()!
|
||||
|
||||
// insertContent discards the incomplete binding
|
||||
const size = app.shapes.length
|
||||
|
||||
const before = [...app.shapes]
|
||||
|
||||
app.insertContent(content)
|
||||
|
||||
expect(app.shapes.length).toBe(size + 1)
|
||||
|
||||
const inserted = [...app.shapes].filter((s) => !before.includes(s))[0]
|
||||
|
||||
expect(inserted.parentId).toBe(app.currentPageId)
|
||||
})
|
||||
|
||||
it('does not add groups without children', () => {
|
||||
// insertContent discards the incomplete binding
|
||||
const size = app.shapes.length
|
||||
|
||||
app.insertContent({
|
||||
shapes: [
|
||||
{
|
||||
id: '935ff424-bf40-4e2d-3bfb-d26061150b03',
|
||||
type: TDShapeType.Group,
|
||||
name: 'Group',
|
||||
parentId: 'currentPageId',
|
||||
childIndex: 1,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
rotation: 0,
|
||||
children: [],
|
||||
style: {
|
||||
color: ColorStyle.Black,
|
||||
size: SizeStyle.Small,
|
||||
isFilled: false,
|
||||
dash: DashStyle.Dashed,
|
||||
scale: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(app.shapes.length).toBe(size)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,251 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { GRID_SIZE } from '~constants'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import type { PagePartial, TldrawCommand, TDShape, TDBinding, TDAsset } from '~types'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
|
||||
export function insertContent(
|
||||
app: TldrawApp,
|
||||
content: { shapes: TDShape[]; bindings?: TDBinding[]; assets?: TDAsset[] },
|
||||
opts = {} as { point?: number[]; select?: boolean }
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app
|
||||
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
}
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
}
|
||||
|
||||
const oldToNewIds: Record<string, string> = {}
|
||||
|
||||
// The index of the new shape
|
||||
let nextIndex = TLDR.getTopChildIndex(app.state, currentPageId)
|
||||
|
||||
const shapesToInsert: TDShape[] = content.shapes
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => {
|
||||
const newShapeId = Utils.uniqueId()
|
||||
oldToNewIds[shape.id] = newShapeId
|
||||
|
||||
// The redo should include a clone of the new shape
|
||||
return {
|
||||
...Utils.deepClone(shape),
|
||||
id: newShapeId,
|
||||
}
|
||||
})
|
||||
|
||||
const visited = new Set<string>()
|
||||
|
||||
// Iterate through the list, starting from the front
|
||||
while (shapesToInsert.length > 0) {
|
||||
const shape = shapesToInsert.shift()
|
||||
|
||||
if (!shape) break
|
||||
|
||||
visited.add(shape.id)
|
||||
|
||||
if (shape.parentId === 'currentPageId') {
|
||||
shape.parentId = currentPageId
|
||||
shape.childIndex = nextIndex++
|
||||
} else {
|
||||
// The shape had another shape as its parent.
|
||||
|
||||
// Re-assign the shape's parentId to the new id
|
||||
shape.parentId = oldToNewIds[shape.parentId]
|
||||
|
||||
// Has that parent been added yet to the after object?
|
||||
const parent = after.shapes[shape.parentId]
|
||||
|
||||
if (!parent) {
|
||||
if (visited.has(shape.id)) {
|
||||
// If we've already visited this shape, then that means
|
||||
// its parent was not among the shapes to insert. Set it
|
||||
// to be a child of the current page instead.
|
||||
shape.parentId = 'currentPageId'
|
||||
}
|
||||
|
||||
// If the parent hasn't been added yet, push this shape
|
||||
// to back of the queue; we'll try and add it again later
|
||||
shapesToInsert.push(shape)
|
||||
continue
|
||||
}
|
||||
|
||||
// If we've found the parent, add this shape's id to its children
|
||||
parent.children!.push(shape.id)
|
||||
}
|
||||
|
||||
// If the inserting shape has its own children, set the children to
|
||||
// an empty array; we'll add them later, as just shown above
|
||||
if (shape.children) {
|
||||
shape.children = []
|
||||
}
|
||||
|
||||
// The undo should remove the inserted shape
|
||||
before.shapes[shape.id] = undefined
|
||||
|
||||
// The redo should include the inserted shape
|
||||
after.shapes[shape.id] = shape
|
||||
}
|
||||
|
||||
Object.values(after.shapes).forEach((shape) => {
|
||||
// If the shape used to have children, but no longer does have children,
|
||||
// then delete the shape. This prevents inserting groups without children.
|
||||
if (shape!.children && shape!.children.length === 0) {
|
||||
delete before.shapes[shape!.id!]
|
||||
delete after.shapes[shape!.id!]
|
||||
}
|
||||
})
|
||||
|
||||
// Insert bindings
|
||||
if (content.bindings) {
|
||||
content.bindings.forEach((binding) => {
|
||||
const newBindingId = Utils.uniqueId()
|
||||
oldToNewIds[binding.id] = newBindingId
|
||||
|
||||
const toId = oldToNewIds[binding.toId]
|
||||
const fromId = oldToNewIds[binding.fromId]
|
||||
|
||||
// If the binding is "to" or "from" a shape that hasn't been inserted,
|
||||
// we'll need to skip the binding and remove it from any shape that
|
||||
// references it.
|
||||
if (!toId || !fromId) {
|
||||
if (fromId) {
|
||||
const handles = after.shapes[fromId]!.handles
|
||||
if (handles) {
|
||||
Object.values(handles).forEach((handle) => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (toId) {
|
||||
const handles = after.shapes[toId]!.handles
|
||||
if (handles) {
|
||||
Object.values(handles).forEach((handle) => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Update the shape's to and from references to the new bindingid
|
||||
|
||||
const fromHandles = after.shapes[fromId]!.handles
|
||||
if (fromHandles) {
|
||||
Object.values(fromHandles).forEach((handle) => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = newBindingId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const toHandles = after.shapes[toId]!.handles
|
||||
if (toHandles) {
|
||||
Object.values(after.shapes[toId]!.handles!).forEach((handle) => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = newBindingId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const newBinding = {
|
||||
...Utils.deepClone(binding),
|
||||
id: newBindingId,
|
||||
toId,
|
||||
fromId,
|
||||
}
|
||||
|
||||
// The undo should remove the inserted binding
|
||||
before.bindings[newBinding.id] = undefined
|
||||
|
||||
// The redo should include the inserted binding
|
||||
after.bindings[newBinding.id] = newBinding
|
||||
})
|
||||
}
|
||||
|
||||
// Now move the shapes
|
||||
|
||||
const shapesToMove = Object.values(after.shapes) as TDShape[]
|
||||
|
||||
const { point, select } = opts
|
||||
|
||||
if (shapesToMove.length > 0) {
|
||||
if (point) {
|
||||
// Move the shapes so that they're centered on the given point
|
||||
const commonBounds = Utils.getCommonBounds(shapesToMove.map((shape) => TLDR.getBounds(shape)))
|
||||
const center = Utils.getBoundsCenter(commonBounds)
|
||||
shapesToMove.forEach((shape) => {
|
||||
if (!shape.point) return
|
||||
shape.point = Vec.sub(point, Vec.sub(center, shape.point))
|
||||
})
|
||||
} else {
|
||||
const commonBounds = Utils.getCommonBounds(shapesToMove.map(TLDR.getBounds))
|
||||
|
||||
if (
|
||||
!(
|
||||
Utils.boundsContain(app.viewport, commonBounds) ||
|
||||
Utils.boundsCollide(app.viewport, commonBounds)
|
||||
)
|
||||
) {
|
||||
const center = Vec.toFixed(app.getPagePoint(app.centerPoint))
|
||||
|
||||
const centeredBounds = Utils.centerBounds(commonBounds, center)
|
||||
|
||||
const delta = Vec.sub(
|
||||
Utils.getBoundsCenter(centeredBounds),
|
||||
Utils.getBoundsCenter(commonBounds)
|
||||
)
|
||||
|
||||
shapesToMove.forEach((shape) => {
|
||||
shape.point = Vec.toFixed(Vec.add(shape.point, delta))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'insert',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: before,
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: { selectedIds: [...app.selectedIds] },
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: after,
|
||||
},
|
||||
assets: content.assets
|
||||
? Object.fromEntries(
|
||||
content.assets
|
||||
.filter((asset) => !app.document.assets[asset.id])
|
||||
.map((asset) => [asset.id, asset])
|
||||
)
|
||||
: {},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: select ? Object.keys(after.shapes) : [...app.selectedIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue