diff --git a/assets/translations/main.json b/assets/translations/main.json index a0c97d07a..f8679b009 100644 --- a/assets/translations/main.json +++ b/assets/translations/main.json @@ -53,6 +53,7 @@ "action.paste": "Paste", "action.print": "Print", "action.redo": "Redo", + "action.remove-frame": "Remove frame", "action.rotate-ccw": "Rotate counterclockwise", "action.rotate-cw": "Rotate clockwise", "action.save-copy": "Save a copy", diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 564bfd78e..e570b8579 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -147,6 +147,8 @@ export abstract class BaseBoxShapeTool extends StateNode { // (undocumented) static initial: string; // (undocumented) + onCreate?: (_shape: null | TLShape) => null | void; + // (undocumented) abstract shapeType: string; } @@ -885,6 +887,7 @@ export class Editor extends EventEmitter { registerExternalContentHandler(type: T, handler: ((info: T extends TLExternalContent['type'] ? TLExternalContent & { type: T; } : TLExternalContent) => void) | null): this; + removeFrame(ids: TLShapeId[]): this; renamePage(page: TLPage | TLPageId, name: string, historyOptions?: TLCommandHistoryOptions): this; // @deprecated (undocumented) get renderingBounds(): Box2d; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 4131a76e1..f06666529 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -1083,6 +1083,45 @@ "isProtected": false, "isAbstract": false }, + { + "kind": "Property", + "canonicalReference": "@tldraw/editor!BaseBoxShapeTool#onCreate:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "onCreate?: " + }, + { + "kind": "Content", + "text": "(_shape: null | " + }, + { + "kind": "Reference", + "text": "TLShape", + "canonicalReference": "@tldraw/tlschema!TLShape:type" + }, + { + "kind": "Content", + "text": ") => null | void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "onCreate", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "@tldraw/editor!BaseBoxShapeTool#shapeType:member", @@ -16351,6 +16390,59 @@ "isAbstract": false, "name": "registerExternalContentHandler" }, + { + "kind": "Method", + "canonicalReference": "@tldraw/editor!Editor#removeFrame:member(1)", + "docComment": "/**\n * Remove a frame.\n *\n * @param ids - Ids of the frames you wish to remove.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "removeFrame(ids: " + }, + { + "kind": "Reference", + "text": "TLShapeId", + "canonicalReference": "@tldraw/tlschema!TLShapeId:type" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "this" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": false, + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "ids", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "removeFrame" + }, { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#renamePage:member(1)", @@ -19385,7 +19477,7 @@ { "kind": "Method", "canonicalReference": "@tldraw/editor!Editor#updateCurrentPageState:member(1)", - "docComment": "/**\n * Update this instance's page state.\n *\n * @param partial - The partial of the page state object containing the changes.\n *\n * @param historyOptions - The history options for the change.\n *\n * @example\n * ```ts\n * editor.updateInstancePageState({ id: 'page1', editingShapeId: 'shape:123' })\n * editor.updateInstancePageState({ id: 'page1', editingShapeId: 'shape:123' }, { ephemeral: true })\n * ```\n *\n * @public\n */\n", + "docComment": "/**\n * Update this instance's page state.\n *\n * @param partial - The partial of the page state object containing the changes.\n *\n * @param historyOptions - The history options for the change.\n *\n * @example\n * ```ts\n * editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' })\n * editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' }, { ephemeral: true })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", diff --git a/packages/editor/editor.css b/packages/editor/editor.css index 82b6f4235..e8a999971 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -1315,6 +1315,11 @@ input, stroke-width: calc(1px * var(--tl-scale)); } +.tl-frame__creating { + stroke: var(--color-selected); + fill: none; +} + .tl-frame__hitarea { border-style: solid; border-width: calc(8px * var(--tl-scale)); diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 3a22f4e87..a677a180a 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -1467,8 +1467,8 @@ export class Editor extends EventEmitter { * * @example * ```ts - * editor.updateInstancePageState({ id: 'page1', editingShapeId: 'shape:123' }) - * editor.updateInstancePageState({ id: 'page1', editingShapeId: 'shape:123' }, { ephemeral: true }) + * editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' }) + * editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' }, { ephemeral: true }) * ``` * * @param partial - The partial of the page state object containing the changes. @@ -5159,6 +5159,8 @@ export class Editor extends EventEmitter { reparentShapes(shapes: TLShapeId[] | TLShape[], parentId: TLParentId, insertIndex?: string) { const ids = typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : shapes.map((s) => (s as TLShape).id) + if (ids.length === 0) return this + const changes: TLShapePartial[] = [] const parentTransform = isPageId(parentId) @@ -7317,6 +7319,36 @@ export class Editor extends EventEmitter { return this } + /** + * Remove a frame. + * + * @param ids - Ids of the frames you wish to remove. + * + * @public + */ + removeFrame(ids: TLShapeId[]): this { + const frames = compact( + ids + .map((id) => this.getShape(id)) + .filter((f) => f && this.isShapeOfType(f, 'frame')) + ) + if (!frames.length) return this + + const allChildren: TLShapeId[] = [] + this.batch(() => { + frames.map((frame) => { + const children = this.getSortedChildIdsForParent(frame.id) + if (children.length) { + this.reparentShapes(children, frame.parentId, frame.index) + allChildren.push(...children) + } + }) + this.setSelectedShapes(allChildren) + this.deleteShapes(ids) + }) + return this + } + /** * Update a shape using a partial of the shape. * diff --git a/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts b/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts index 4742dced0..0d4ceae3d 100644 --- a/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts +++ b/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool.ts @@ -1,3 +1,4 @@ +import { TLShape } from '@tldraw/tlschema' import { StateNode } from '../StateNode' import { Idle } from './children/Idle' import { Pointing } from './children/Pointing' @@ -9,4 +10,6 @@ export abstract class BaseBoxShapeTool extends StateNode { static override children = () => [Idle, Pointing] abstract override shapeType: string + + onCreate?: (_shape: TLShape | null) => void | null } diff --git a/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts b/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts index 13acb209d..a34f14676 100644 --- a/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +++ b/packages/editor/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts @@ -49,6 +49,7 @@ export class Pointing extends StateNode { isCreating: true, creationCursorOffset: { x: 1, y: 1 }, onInteractionEnd: this.parent.id, + onCreate: (this.parent as BaseBoxShapeTool).onCreate, }) } } diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 33878b68f..6ca16fe84 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -484,6 +484,8 @@ export class FrameShapeTool extends BaseBoxShapeTool { // (undocumented) static initial: string; // (undocumented) + onCreate: (shape: null | TLShape) => void; + // (undocumented) shapeType: string; } @@ -514,6 +516,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil { shouldHint: boolean; }; // (undocumented) + onResize: TLOnResizeHandler; + // (undocumented) onResizeEnd: TLOnResizeEndHandler; // (undocumented) static props: { @@ -1482,6 +1486,8 @@ export interface TLUiEventMap { // (undocumented) 'pack-shapes': null; // (undocumented) + 'remove-frame': null; + // (undocumented) 'reorder-shapes': { operation: 'backward' | 'forward' | 'toBack' | 'toFront'; }; @@ -1792,7 +1798,7 @@ export type TLUiTranslation = { export type TLUiTranslationContextType = TLUiTranslation; // @public (undocumented) -export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; +export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; // @public (undocumented) export function toolbarItem(toolItem: TLUiToolItem): TLUiToolbarItem; diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index d58ff7f8e..409ff0b46 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -5612,6 +5612,45 @@ "isProtected": false, "isAbstract": false }, + { + "kind": "Property", + "canonicalReference": "@tldraw/tldraw!FrameShapeTool#onCreate:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "onCreate: " + }, + { + "kind": "Content", + "text": "(shape: null | " + }, + { + "kind": "Reference", + "text": "TLShape", + "canonicalReference": "@tldraw/tlschema!TLShape:type" + }, + { + "kind": "Content", + "text": ") => void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "onCreate", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "@tldraw/tldraw!FrameShapeTool#shapeType:member", @@ -6160,6 +6199,41 @@ "isProtected": false, "isAbstract": false }, + { + "kind": "Property", + "canonicalReference": "@tldraw/tldraw!FrameShapeUtil#onResize:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "onResize: " + }, + { + "kind": "Reference", + "text": "TLOnResizeHandler", + "canonicalReference": "@tldraw/editor!TLOnResizeHandler:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "onResize", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "@tldraw/tldraw!FrameShapeUtil#onResizeEnd:member", @@ -16529,6 +16603,33 @@ "endIndex": 2 } }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"remove-frame\":member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "'remove-frame': " + }, + { + "kind": "Content", + "text": "null" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "\"remove-frame\"", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"reorder-shapes\":member", @@ -19960,7 +20061,7 @@ }, { "kind": "Content", - "text": "'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'" + "text": "'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'" }, { "kind": "Content", diff --git a/packages/tldraw/src/lib/shapes/frame/FrameShapeTool.ts b/packages/tldraw/src/lib/shapes/frame/FrameShapeTool.ts index de18f32aa..987958a81 100644 --- a/packages/tldraw/src/lib/shapes/frame/FrameShapeTool.ts +++ b/packages/tldraw/src/lib/shapes/frame/FrameShapeTool.ts @@ -1,8 +1,53 @@ -import { BaseBoxShapeTool } from '@tldraw/editor' +import { BaseBoxShapeTool, TLShape, TLShapeId } from '@tldraw/editor' /** @public */ export class FrameShapeTool extends BaseBoxShapeTool { static override id = 'frame' static override initial = 'idle' override shapeType = 'frame' + + override onCreate = (shape: TLShape | null): void => { + if (!shape) return + + const bounds = this.editor.getShapePageBounds(shape)! + const shapesToAddToFrame: TLShapeId[] = [] + const ancestorIds = this.editor.getShapeAncestors(shape).map((shape) => shape.id) + + this.editor.getCurrentPageShapes().map((pageShape) => { + // We don't want to frame the frame itself + if (pageShape.id === shape.id) return + if (pageShape.isLocked) return + + const pageShapeBounds = this.editor.getShapePageBounds(pageShape) + if (!pageShapeBounds) return + + // Frame shape encloses page shape + if (bounds.contains(pageShapeBounds)) { + if (canEnclose(pageShape, ancestorIds, shape)) { + shapesToAddToFrame.push(pageShape.id) + } + } + }) + + this.editor.reparentShapes(shapesToAddToFrame, shape.id) + + if (this.editor.getInstanceState().isToolLocked) { + this.editor.setCurrentTool('frame') + } else { + this.editor.setCurrentTool('select.idle') + } + } +} + +/** @internal */ +function canEnclose(shape: TLShape, ancestorIds: TLShapeId[], frame: TLShape): boolean { + // We don't want to pull in shapes that are ancestors of the frame (can create a cycle) + if (ancestorIds.includes(shape.id)) { + return false + } + // We only want to pull in shapes that are siblings of the frame + if (shape.parentId === frame.parentId) { + return true + } + return false } diff --git a/packages/tldraw/src/lib/shapes/frame/FrameShapeUtil.tsx b/packages/tldraw/src/lib/shapes/frame/FrameShapeUtil.tsx index 5d973b318..ba436e443 100644 --- a/packages/tldraw/src/lib/shapes/frame/FrameShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/frame/FrameShapeUtil.tsx @@ -7,6 +7,7 @@ import { TLFrameShape, TLGroupShape, TLOnResizeEndHandler, + TLOnResizeHandler, TLShape, TLShapeId, canonicalizeRotation, @@ -14,8 +15,11 @@ import { frameShapeProps, getDefaultColorTheme, last, + resizeBox, toDomPrecision, + useValue, } from '@tldraw/editor' +import classNames from 'classnames' import { useDefaultColorTheme } from '../shared/ShapeFill' import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans' import { FrameHeading } from './components/FrameHeading' @@ -54,23 +58,40 @@ export class FrameShapeUtil extends BaseBoxShapeUtil { // eslint-disable-next-line react-hooks/rules-of-hooks const theme = useDefaultColorTheme() + // eslint-disable-next-line react-hooks/rules-of-hooks + const isCreating = useValue( + 'is creating this shape', + () => { + const resizingState = this.editor.getStateDescendant('select.resizing') + if (!resizingState) return false + if (!resizingState.getIsActive()) return false + const info = (resizingState as typeof resizingState & { info: { isCreating: boolean } }) + ?.info + if (!info) return false + return info.isCreating && this.editor.getOnlySelectedShape()?.id === shape.id + }, + [shape.id] + ) + return ( <> - + {isCreating ? null : ( + + )} ) } @@ -230,4 +251,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil { this.editor.reparentShapes(shapesToReparent, this.editor.getCurrentPageId()) } } + + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } } diff --git a/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts index c9efe1f4f..171c61a19 100644 --- a/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/note/toolStates/Pointing.ts @@ -37,9 +37,12 @@ export class Pointing extends StateNode { ...info, target: 'shape', shape: this.shape, - isCreating: true, - editAfterComplete: true, onInteractionEnd: 'note', + isCreating: true, + onCreate: () => { + this.editor.setEditingShape(this.shape.id) + this.editor.setCurrentTool('select.editing_shape') + }, }) } } diff --git a/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts index 9ab4b3f52..1fc8bb648 100644 --- a/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/text/toolStates/Pointing.ts @@ -41,14 +41,19 @@ export class Pointing extends StateNode { this.shape = this.editor.getShape(id) if (!this.shape) return + const { shape } = this + this.editor.setCurrentTool('select.resizing', { ...info, target: 'selection', handle: 'right', isCreating: true, creationCursorOffset: { x: 1, y: 1 }, - editAfterComplete: true, onInteractionEnd: 'text', + onCreate: () => { + this.editor.setEditingShape(shape.id) + this.editor.setCurrentTool('select.editing_shape') + }, }) } } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts index a4af21a27..3f0bcb0b7 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts @@ -16,13 +16,14 @@ import { Vec2d, VecLike, areAnglesCompatible, + compact, } from '@tldraw/editor' type ResizingInfo = TLPointerEventInfo & { target: 'selection' handle: SelectionEdge | SelectionCorner isCreating?: boolean - editAfterComplete?: boolean + onCreate?: (shape: TLShape | null) => void creationCursorOffset?: VecLike onInteractionEnd?: string } @@ -34,41 +35,39 @@ export class Resizing extends StateNode { markId = '' + // A switch to detect when the user is holding ctrl + private didHoldCommand = false + // we transition into the resizing state from the geo pointing state, which starts with a shape of size w: 1, h: 1, // so if the user drags x: +50, y: +50 after mouseDown, the shape will be w: 51, h: 51, which is too many pixels, alas // so we allow passing a further offset into this state to negate such issues creationCursorOffset = { x: 0, y: 0 } as VecLike - editAfterComplete = false private snapshot = {} as any as Snapshot override onEnter: TLEnterEventHandler = (info: ResizingInfo) => { - const { - isCreating = false, - editAfterComplete = false, - creationCursorOffset = { x: 0, y: 0 }, - } = info + const { isCreating = false, creationCursorOffset = { x: 0, y: 0 } } = info this.info = info + this.didHoldCommand = false this.parent.setCurrentToolIdMask(info.onInteractionEnd) - this.editAfterComplete = editAfterComplete this.creationCursorOffset = creationCursorOffset - if (info.isCreating) { + this.snapshot = this._createSnapshot() + + if (isCreating) { + this.markId = `creating:${this.editor.getOnlySelectedShape()!.id}` + this.editor.updateInstanceState( { cursor: { type: 'cross', rotation: 0 } }, { ephemeral: true } ) + } else { + this.markId = 'starting resizing' + this.editor.mark(this.markId) } - this.snapshot = this._createSnapshot() - this.markId = isCreating - ? `creating:${this.editor.getOnlySelectedShape()!.id}` - : 'starting resizing' - - if (!isCreating) this.editor.mark(this.markId) - this.handleResizeStart() this.updateShapes() } @@ -109,10 +108,8 @@ export class Resizing extends StateNode { private complete() { this.handleResizeEnd() - const onlySelectedShape = this.editor.getOnlySelectedShape() - if (this.editAfterComplete && onlySelectedShape) { - this.editor.setEditingShape(onlySelectedShape.id) - this.editor.setCurrentTool('select.editing_shape') + if (this.info.isCreating && this.info.onCreate) { + this.info.onCreate?.(this.editor.getOnlySelectedShape()) return } @@ -164,6 +161,7 @@ export class Resizing extends StateNode { private updateShapes() { const { altKey, shiftKey } = this.editor.inputs const { + frames, shapeSnapshots, selectionBounds, cursorHandleOffset, @@ -316,6 +314,48 @@ export class Resizing extends StateNode { scaleAxisRotation: selectionRotation, }) } + + if (this.editor.inputs.ctrlKey) { + this.didHoldCommand = true + + for (const { id, children } of frames) { + if (!children.length) continue + const initial = shapeSnapshots.get(id)!.shape + const current = this.editor.getShape(id)! + if (!(initial && current)) continue + + // If the user is holding ctrl, then preseve the position of the frame's children + const dx = current.x - initial.x + const dy = current.y - initial.y + + const delta = new Vec2d(dx, dy).rot(-initial.rotation) + + if (delta.x !== 0 || delta.y !== 0) { + for (const child of children) { + this.editor.updateShape({ + id: child.id, + type: child.type, + x: child.x - delta.x, + y: child.y - delta.y, + }) + } + } + } + } else if (this.didHoldCommand) { + this.didHoldCommand = false + + for (const { children } of frames) { + if (!children.length) continue + for (const child of children) { + this.editor.updateShape({ + id: child.id, + type: child.type, + x: child.x, + y: child.y, + }) + } + } + } } // --- @@ -385,9 +425,19 @@ export class Resizing extends StateNode { const shapeSnapshots = new Map() + const frames: { id: TLShapeId; children: TLShape[] }[] = [] + selectedShapeIds.forEach((id) => { const shape = this.editor.getShape(id) if (shape) { + if (shape.type === 'frame') { + frames.push({ + id, + children: compact( + this.editor.getSortedChildIdsForParent(shape).map((id) => this.editor.getShape(id)) + ), + }) + } shapeSnapshots.set(shape.id, this._createShapeSnapshot(shape)) if ( this.editor.isShapeOfType(shape, 'frame') && @@ -419,6 +469,7 @@ export class Resizing extends StateNode { selectedShapeIds, canShapesDeform, initialSelectionPageBounds: this.editor.getSelectionPageBounds()!, + frames, } } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts index d7f1d86f9..1465ca74a 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts @@ -22,7 +22,7 @@ export class Translating extends StateNode { info = {} as TLPointerEventInfo & { target: 'shape' isCreating?: boolean - editAfterComplete?: boolean + onCreate?: () => void onInteractionEnd?: string } @@ -34,7 +34,7 @@ export class Translating extends StateNode { isCloning = false isCreating = false - editAfterComplete = false + onCreate: (shape: TLShape | null) => void = () => void null dragAndDropManager = new DragAndDropManager(this.editor) @@ -42,19 +42,24 @@ export class Translating extends StateNode { info: TLPointerEventInfo & { target: 'shape' isCreating?: boolean - editAfterComplete?: boolean + onCreate?: () => void onInteractionEnd?: string } ) => { - const { isCreating = false, editAfterComplete = false } = info + const { isCreating = false, onCreate = () => void null } = info this.info = info this.parent.setCurrentToolIdMask(info.onInteractionEnd) this.isCreating = isCreating - this.editAfterComplete = editAfterComplete + this.onCreate = onCreate + + if (isCreating) { + this.markId = `creating:${this.editor.getOnlySelectedShape()!.id}` + } else { + this.markId = 'translating' + this.editor.mark(this.markId) + } - this.markId = isCreating ? `creating:${this.editor.getOnlySelectedShape()!.id}` : 'translating' - this.editor.mark(this.markId) this.isCloning = false this.info = info @@ -165,12 +170,8 @@ export class Translating extends StateNode { if (this.editor.getInstanceState().isToolLocked && this.info.onInteractionEnd) { this.editor.setCurrentTool(this.info.onInteractionEnd) } else { - if (this.editAfterComplete) { - const onlySelected = this.editor.getOnlySelectedShape() - if (onlySelected) { - this.editor.setEditingShape(onlySelected.id) - this.editor.setCurrentTool('select.editing_shape') - } + if (this.isCreating) { + this.onCreate?.(this.editor.getOnlySelectedShape()) } else { this.parent.transition('idle') } diff --git a/packages/tldraw/src/lib/ui/hooks/useActions.tsx b/packages/tldraw/src/lib/ui/hooks/useActions.tsx index 4b1e36a30..eaa83ca05 100644 --- a/packages/tldraw/src/lib/ui/hooks/useActions.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useActions.tsx @@ -5,6 +5,7 @@ import { TAU, TLBookmarkShape, TLEmbedShape, + TLFrameShape, TLGroupShape, TLShapeId, TLShapePartial, @@ -455,6 +456,25 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { } }, }, + { + id: 'remove-frame', + label: 'action.remove-frame', + kbd: '$!f', + readonlyOk: false, + onSelect(source) { + if (!hasSelectedShapes()) return + + trackEvent('remove-frame', { source }) + const selectedShapes = editor.getSelectedShapes() + if ( + selectedShapes.length > 0 && + selectedShapes.every((shape) => editor.isShapeOfType(shape, 'frame')) + ) { + editor.mark('remove-frame') + editor.removeFrame(selectedShapes.map((shape) => shape.id)) + } + }, + }, { id: 'align-left', label: 'action.align-left', diff --git a/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx b/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx index 81c3ed211..3f5d1d80c 100644 --- a/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx @@ -1,4 +1,4 @@ -import { Editor, track, useEditor, useValue } from '@tldraw/editor' +import { Editor, TLFrameShape, track, useEditor, useValue } from '@tldraw/editor' import React, { useMemo } from 'react' import { TLUiMenuSchema, @@ -55,7 +55,8 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem const onlyFlippableShapeSelected = useOnlyFlippableShape() - const selectedCount = editor.getSelectedShapeIds().length + const selectedShapes = editor.getSelectedShapes() + const selectedCount = selectedShapes.length const oneSelected = selectedCount > 0 @@ -77,6 +78,9 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem const hasClipboardWrite = Boolean(window.navigator.clipboard?.write) const showEditLink = useHasLinkShapeSelected() const onlySelectedShape = editor.getOnlySelectedShape() + const allowRemoveFrame = + oneSelected && + selectedShapes.every((shape) => editor.isShapeOfType(shape, 'frame')) const isShapeLocked = onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape) const contextTLUiMenuSchema = useMemo(() => { @@ -88,6 +92,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem oneSelected && !isShapeLocked && menuItem(actions['duplicate']), allowGroup && !isShapeLocked && menuItem(actions['group']), allowUngroup && !isShapeLocked && menuItem(actions['ungroup']), + allowRemoveFrame && !isShapeLocked && menuItem(actions['remove-frame']), oneSelected && menuItem(actions['toggle-lock']) ), menuGroup( @@ -221,6 +226,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem threeStackableItems, allowGroup, allowUngroup, + allowRemoveFrame, hasClipboardWrite, showEditLink, // oneEmbedSelected, diff --git a/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx b/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx index 67307944c..755bef1a2 100644 --- a/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx @@ -27,6 +27,7 @@ export interface TLUiEventMap { redo: null 'group-shapes': null 'ungroup-shapes': null + 'remove-frame': null 'convert-to-embed': null 'convert-to-bookmark': null 'open-embed-link': null diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts index 9b68e8806..7331c8408 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts @@ -57,6 +57,7 @@ export type TLUiTranslationKey = | 'action.paste' | 'action.print' | 'action.redo' + | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts index db707a8f3..e1b986460 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts @@ -57,6 +57,7 @@ export const DEFAULT_TRANSLATION = { 'action.paste': 'Paste', 'action.print': 'Print', 'action.redo': 'Redo', + 'action.remove-frame': 'Remove frame', 'action.rotate-ccw': 'Rotate counterclockwise', 'action.rotate-cw': 'Rotate clockwise', 'action.save-copy': 'Save a copy', diff --git a/packages/tldraw/src/test/frames.test.ts b/packages/tldraw/src/test/frames.test.ts index a393acb00..9aad52e49 100644 --- a/packages/tldraw/src/test/frames.test.ts +++ b/packages/tldraw/src/test/frames.test.ts @@ -1,4 +1,10 @@ -import { DefaultFillStyle, TLArrowShape, TLFrameShape, createShapeId } from '@tldraw/editor' +import { + DefaultFillStyle, + TLArrowShape, + TLFrameShape, + TLShapeId, + createShapeId, +} from '@tldraw/editor' import { TestEditor } from './TestEditor' let editor: TestEditor @@ -110,6 +116,22 @@ describe('creating frames', () => { }) }) + it('parents a shape when drag-creating a frame over it', () => { + const rectId: TLShapeId = createRect({ pos: [10, 10], size: [20, 20] }) + const frameId = dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] }) + const parent = editor.getShape(rectId)?.parentId + expect(parent).toBe(frameId) + }) + + it('does not parent a shape when click-creating a frame over it', () => { + const rectId: TLShapeId = createRect({ pos: [10, 10], size: [20, 20] }) + editor.setCurrentTool('frame') + editor.pointerDown(0, 0) + editor.pointerUp(0, 0) + const parent = editor.getShape(rectId)?.parentId + expect(parent).toBe('page:page') + }) + it('can snap', () => { editor.createShapes([ { type: 'geo', id: ids.boxA, x: 0, y: 0, props: { w: 50, h: 50, fill: 'solid' } }, @@ -234,6 +256,61 @@ describe('frame shapes', () => { h: 50, }) }) + it('unparents a shape when resize causes it to be out of bounds', () => { + const rectId: TLShapeId = createRect({ pos: [70, 10], size: [20, 20] }) + dragCreateFrame({ down: [10, 10], move: [100, 100], up: [100, 100] }) + // resize the frame so the shape is out of bounds + editor.pointerDown(100, 50, { target: 'selection', handle: 'right' }) + editor.pointerMove(50, 50) + editor.pointerUp(50, 50) + const parent = editor.getShape(rectId)?.parentId + expect(parent).toBe('page:page') + }) + + it('doesnt unparent a shape that is only partially out of bounds', () => { + const rectId: TLShapeId = createRect({ pos: [70, 10], size: [20, 20] }) + const frameId = dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] }) + const parentBefore = editor.getShape(rectId)?.parentId + expect(parentBefore).toBe(frameId) + // resize the frame so the shape is partially out of bounds + editor.pointerDown(100, 50, { target: 'selection', handle: 'right' }) + editor.pointerMove(70, 50) + editor.pointerUp(70, 50) + const parentAfter = editor.getShape(rectId)?.parentId + expect(parentAfter).toBe(frameId) + }) + + it('does not parent a shape when resizing over it', () => { + const rectId = createRect({ pos: [70, 10], size: [20, 20] }) + // create frame next to shape + dragCreateFrame({ down: [10, 10], move: [60, 100], up: [60, 100] }) + // resize the frame so the shape is totally covered + editor.pointerDown(60, 50, { target: 'selection', handle: 'right' }) + editor.pointerMove(100, 50) + editor.pointerUp(100, 50) + const parent = editor.getShape(rectId)?.parentId + expect(parent).toBe('page:page') + }) + + it('moves children when resizing a parent frame', () => { + const rectId: TLShapeId = createRect({ size: [20, 20], pos: [10, 10] }) + dragCreateFrame({ down: [10, 10], move: [100, 100], up: [100, 100] }) + editor.pointerDown(0, 0, { target: 'selection', handle: 'top_left' }) + expect(editor.getShape(rectId)?.y).toBe(10) + editor.pointerMove(-50, -50) + editor.pointerUp(-50, -50) + expect(editor.getShape(rectId)?.y).toBe(10) + }) + + it('does not move children when resizing with cmd key held down', () => { + const rectId: TLShapeId = createRect({ size: [20, 20], pos: [10, 10] }) + dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] }) + editor.pointerDown(0, 0, { target: 'selection', handle: 'top_left' }) + editor.keyDown('Control') + editor.pointerMove(-50, -50) + editor.pointerUp(-50, -50) + expect(editor.getShape(rectId)?.x).toBe(60) + }) it('can have shapes dragged on top and back out', () => { editor.setCurrentTool('frame') @@ -775,3 +852,79 @@ describe('When dragging a shape inside a group inside a frame', () => { expect(editor.getShape(ids.box1)!.parentId).toBe(editor.getCurrentPageId()) }) }) + +describe('When deleting/removing a frame', () => { + it('deletes a frame and its children', () => { + const rectId: TLShapeId = createRect({ size: [20, 20], pos: [10, 10] }) + const frameId = dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] }) + editor.deleteShape(frameId) + expect(editor.getShape(rectId)).toBeUndefined() + }) + it('removes a frame but not its children', () => { + const rectId: TLShapeId = createRect({ size: [20, 20], pos: [10, 10] }) + const frameId = dragCreateFrame({ down: [10, 10], move: [100, 100], up: [100, 100] }) + editor.removeFrame([frameId]) + expect(editor.getShape(rectId)).toBeDefined() + }) + it('reparents the children of a frame when removing it', () => { + const rectId: TLShapeId = createRect({ size: [20, 20], pos: [10, 10] }) + const frame1Id = dragCreateFrame({ down: [10, 10], move: [100, 100], up: [100, 100] }) + const frame2Id = dragCreateFrame({ down: [0, 0], move: [110, 110], up: [110, 110] }) + editor.removeFrame([frame1Id]) + expect(editor.getShape(rectId)?.parentId).toBe(frame2Id) + }) +}) + +describe('When dragging a shape', () => { + it('parents a shape when dragging it into a frame', () => { + const rectId: TLShapeId = createRect({ pos: [70, 10], size: [20, 20] }) + // create frame next to shape + const frameId = dragCreateFrame({ down: [0, 0], move: [60, 100], up: [60, 100] }) + // drag shape into frame + editor.pointerDown(80, 15) + editor.pointerMove(30, 50) + editor.pointerUp(30, 50) + const parent = editor.getShape(rectId)?.parentId + expect(parent).toBe(frameId) + }) + it('Unparents a shape when dragging it out of a frame', () => { + const rectId: TLShapeId = createRect({ pos: [10, 10], size: [20, 20] }) + editor.pointerDown(15, 15, { target: 'selection' }) + editor.pointerMove(-100, -100) + editor.pointerUp(-100, -100) + const parent = editor.getShape(rectId)?.parentId + expect(parent).toBe('page:page') + }) +}) + +function dragCreateFrame({ + down, + move, + up, +}: { + down: [number, number] + move: [number, number] + up: [number, number] +}): TLShapeId { + editor.setCurrentTool('frame') + editor.pointerDown(...down) + editor.pointerMove(...move) + editor.pointerUp(...up) + const shapes = editor.getSelectedShapes() + const frameId = shapes[0].id + return frameId +} + +function createRect({ pos, size }: { pos: [number, number]; size: [number, number] }) { + const rectId: TLShapeId = createShapeId() + editor.createShapes([ + { + id: rectId, + x: pos[0], + y: pos[1], + props: { w: size[0], h: size[1] }, + type: 'geo', + }, + ]) + return rectId +}