From e2ddbb16f669a05eadd197600a86ab8a4ee37ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Wed, 29 Nov 2023 13:01:57 +0100 Subject: [PATCH] Removing frames and adding elements to frames (#2219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add simple frame removing - it just drops the frame and parent children to frames parent. - Select children after removing the frame. - Add children to the frame if we resize the frame so that it encloses them. Describe what your pull request does. If appropriate, add GIFs or images showing the before and after. ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. --------- Co-authored-by: Steve Ruiz Co-authored-by: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com> --- assets/translations/main.json | 1 + packages/editor/api-report.md | 3 + packages/editor/api/api.json | 94 ++++++++++- packages/editor/editor.css | 5 + packages/editor/src/lib/editor/Editor.ts | 36 +++- .../BaseBoxShapeTool/BaseBoxShapeTool.ts | 3 + .../BaseBoxShapeTool/children/Pointing.ts | 1 + packages/tldraw/api-report.md | 8 +- packages/tldraw/api/api.json | 103 +++++++++++- .../src/lib/shapes/frame/FrameShapeTool.ts | 47 +++++- .../src/lib/shapes/frame/FrameShapeUtil.tsx | 39 ++++- .../lib/shapes/note/toolStates/Pointing.ts | 7 +- .../lib/shapes/text/toolStates/Pointing.ts | 7 +- .../tools/SelectTool/childStates/Resizing.ts | 91 +++++++--- .../SelectTool/childStates/Translating.ts | 27 +-- .../tldraw/src/lib/ui/hooks/useActions.tsx | 20 +++ .../src/lib/ui/hooks/useContextMenuSchema.tsx | 10 +- .../src/lib/ui/hooks/useEventsProvider.tsx | 1 + .../useTranslation/TLUiTranslationKey.ts | 1 + .../useTranslation/defaultTranslation.ts | 1 + packages/tldraw/src/test/frames.test.ts | 155 +++++++++++++++++- 21 files changed, 608 insertions(+), 52 deletions(-) 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 +}