From 300466f52a3eae2f49186c0ac126337d8ec7c9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitja=20Bezen=C5=A1ek?= Date: Thu, 7 Dec 2023 13:57:56 +0100 Subject: [PATCH] Add fit to content for frames. (#2275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Fit to content option for frames. This resizes the frames so that the whole content fits. It also adds 50px padding on all sides so that the content does not touch the frame's borders. https://github.com/tldraw/tldraw/assets/2523721/b2f86e31-7dfb-495f-ac31-f1e0125e0af1 https://github.com/tldraw/tldraw/assets/2523721/e0a73d25-ac9f-4a35-a1fd-4aed7a5b151c Fixes #1407 ### 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 some shapes. 2. Add a frame that encloses those shapes. 3. Right click on the frame and choose `Fit to content` 4. The frame should resize to fit all the children with some padding on all sides of the frame. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Add Fit to content option to the context menu for frames. This resizes the frames to correctly fit all their content. --------- Co-authored-by: David Sheldrick Co-authored-by: Steve Ruiz --- assets/translations/main.json | 1 + packages/editor/api-report.md | 1 - packages/editor/api/api.json | 53 ------ packages/editor/src/lib/editor/Editor.ts | 30 --- packages/tldraw/api-report.md | 12 +- packages/tldraw/api/api.json | 175 +++++++++++++++++- packages/tldraw/src/index.ts | 1 + .../tldraw/src/lib/ui/hooks/useActions.tsx | 21 ++- .../src/lib/ui/hooks/useContextMenuSchema.tsx | 6 + .../src/lib/ui/hooks/useEventsProvider.tsx | 1 + .../useTranslation/TLUiTranslationKey.ts | 1 + .../useTranslation/defaultTranslation.ts | 1 + .../tldraw/src/lib/utils/frames/frames.ts | 101 ++++++++++ packages/tldraw/src/test/frames.test.ts | 69 ++++++- 14 files changed, 384 insertions(+), 89 deletions(-) create mode 100644 packages/tldraw/src/lib/utils/frames/frames.ts diff --git a/assets/translations/main.json b/assets/translations/main.json index f8679b009..58a58ee52 100644 --- a/assets/translations/main.json +++ b/assets/translations/main.json @@ -36,6 +36,7 @@ "action.export-as-png": "Export as PNG", "action.export-as-svg.short": "SVG", "action.export-as-svg": "Export as SVG", + "action.fit-frame-to-content": "Fit to content", "action.flip-horizontal": "Flip horizontally", "action.flip-vertical": "Flip vertically", "action.flip-horizontal.short": "Flip H", diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 483b47231..7fa8ba97e 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -887,7 +887,6 @@ 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 f06666529..47cba068c 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -16390,59 +16390,6 @@ "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)", diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 39c045025..c5aa2753b 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -7317,36 +7317,6 @@ 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/tldraw/api-report.md b/packages/tldraw/api-report.md index 1c8e83aa4..1c7a9210e 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -473,6 +473,11 @@ export function exportAs(editor: Editor, ids: TLShapeId[], format?: TLExportType // @public (undocumented) export function findMenuItem(menu: TLUiMenuSchema, path: string[]): TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu; +// @public +export function fitFrameToContent(editor: Editor, id: TLShapeId, opts?: { + padding: number; +}): void; + // @public (undocumented) function Footer({ className, children }: { className?: string; @@ -1051,6 +1056,9 @@ export function parseTldrawJsonFile({ json, schema, }: { // @public (undocumented) function RadioItem({ children, onSelect, ...rest }: DropdownMenuCheckboxItemProps): JSX.Element; +// @public +export function removeFrame(editor: Editor, ids: TLShapeId[]): void; + // @public (undocumented) function Root({ id, children, modal, debugOpen, }: { id: string; @@ -1466,6 +1474,8 @@ export interface TLUiEventMap { format: 'json' | 'png' | 'svg'; }; // (undocumented) + 'fit-frame-to-content': null; + // (undocumented) 'flip-shapes': { operation: 'horizontal' | 'vertical'; }; @@ -1800,7 +1810,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.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'; +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.fit-frame-to-content' | '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 eeeb5b05d..32c9f934d 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -5527,6 +5527,85 @@ ], "name": "findMenuItem" }, + { + "kind": "Function", + "canonicalReference": "@tldraw/tldraw!fitFrameToContent:function(1)", + "docComment": "/**\n * Fit a frame to its content.\n *\n * @param id - Id of the frame you wish to fit to content.\n *\n * @param editor - tlraw editor instance.\n *\n * @param opts - Options for fitting the frame.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function fitFrameToContent(editor: " + }, + { + "kind": "Reference", + "text": "Editor", + "canonicalReference": "@tldraw/editor!Editor:class" + }, + { + "kind": "Content", + "text": ", id: " + }, + { + "kind": "Reference", + "text": "TLShapeId", + "canonicalReference": "@tldraw/tlschema!TLShapeId:type" + }, + { + "kind": "Content", + "text": ", opts?: " + }, + { + "kind": "Content", + "text": "{\n padding: number;\n}" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/utils/frames/frames.ts", + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "editor", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "id", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "isOptional": false + }, + { + "parameterName": "opts", + "parameterTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "isOptional": true + } + ], + "name": "fitFrameToContent" + }, { "kind": "Class", "canonicalReference": "@tldraw/tldraw!FrameShapeTool:class", @@ -12132,6 +12211,73 @@ ], "name": "parseTldrawJsonFile" }, + { + "kind": "Function", + "canonicalReference": "@tldraw/tldraw!removeFrame:function(1)", + "docComment": "/**\n * Remove a frame.\n *\n * @param editor - tlraw editor instance.\n *\n * @param ids - Ids of the frames you wish to remove.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function removeFrame(editor: " + }, + { + "kind": "Reference", + "text": "Editor", + "canonicalReference": "@tldraw/editor!Editor:class" + }, + { + "kind": "Content", + "text": ", ids: " + }, + { + "kind": "Reference", + "text": "TLShapeId", + "canonicalReference": "@tldraw/tlschema!TLShapeId:type" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/tldraw/src/lib/utils/frames/frames.ts", + "returnTypeTokenRange": { + "startIndex": 6, + "endIndex": 7 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "editor", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + }, + { + "parameterName": "ids", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, + "isOptional": false + } + ], + "name": "removeFrame" + }, { "kind": "Class", "canonicalReference": "@tldraw/tldraw!SelectTool:class", @@ -16360,6 +16506,33 @@ "endIndex": 2 } }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"fit-frame-to-content\":member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "'fit-frame-to-content': " + }, + { + "kind": "Content", + "text": "null" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "\"fit-frame-to-content\"", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"flip-shapes\":member", @@ -20061,7 +20234,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.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'" + "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.fit-frame-to-content' | '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/index.ts b/packages/tldraw/src/index.ts index ffe6fac67..a81f60c7b 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -154,6 +154,7 @@ export { getEmbedInfo } from './lib/utils/embeds/embeds' export { copyAs } from './lib/utils/export/copyAs' export { getSvgAsImage } from './lib/utils/export/export' export { exportAs } from './lib/utils/export/exportAs' +export { fitFrameToContent, removeFrame } from './lib/utils/frames/frames' export { setDefaultEditorAssetUrls } from './lib/utils/static-assets/assetUrls' export { truncateStringWithEllipsis } from './lib/utils/text/text' export { diff --git a/packages/tldraw/src/lib/ui/hooks/useActions.tsx b/packages/tldraw/src/lib/ui/hooks/useActions.tsx index eaa83ca05..6559a5c40 100644 --- a/packages/tldraw/src/lib/ui/hooks/useActions.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useActions.tsx @@ -19,6 +19,7 @@ import { } from '@tldraw/editor' import * as React from 'react' import { getEmbedInfo } from '../../utils/embeds/embeds' +import { fitFrameToContent, removeFrame } from '../../utils/frames/frames' import { EditLinkDialog } from '../components/EditLinkDialog' import { EmbedDialog } from '../components/EmbedDialog' import { TLUiIconType } from '../icon-types' @@ -471,7 +472,25 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { selectedShapes.every((shape) => editor.isShapeOfType(shape, 'frame')) ) { editor.mark('remove-frame') - editor.removeFrame(selectedShapes.map((shape) => shape.id)) + removeFrame( + editor, + selectedShapes.map((shape) => shape.id) + ) + } + }, + }, + { + id: 'fit-frame-to-content', + label: 'action.fit-frame-to-content', + readonlyOk: false, + onSelect(source) { + if (!hasSelectedShapes()) return + + trackEvent('fit-frame-to-content', { source }) + const onlySelectedShape = editor.getOnlySelectedShape() + if (onlySelectedShape && editor.isShapeOfType(onlySelectedShape, 'frame')) { + editor.mark('fit-frame-to-content') + fitFrameToContent(editor, onlySelectedShape.id) } }, }, diff --git a/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx b/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx index 3f5d1d80c..37815283e 100644 --- a/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useContextMenuSchema.tsx @@ -81,6 +81,10 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem const allowRemoveFrame = oneSelected && selectedShapes.every((shape) => editor.isShapeOfType(shape, 'frame')) + const allowFitFrameToContent = + onlySelectedShape && + editor.isShapeOfType(onlySelectedShape, 'frame') && + editor.getSortedChildIdsForParent(onlySelectedShape).length > 0 const isShapeLocked = onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape) const contextTLUiMenuSchema = useMemo(() => { @@ -93,6 +97,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem allowGroup && !isShapeLocked && menuItem(actions['group']), allowUngroup && !isShapeLocked && menuItem(actions['ungroup']), allowRemoveFrame && !isShapeLocked && menuItem(actions['remove-frame']), + allowFitFrameToContent && !isShapeLocked && menuItem(actions['fit-frame-to-content']), oneSelected && menuItem(actions['toggle-lock']) ), menuGroup( @@ -227,6 +232,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem allowGroup, allowUngroup, allowRemoveFrame, + allowFitFrameToContent, hasClipboardWrite, showEditLink, // oneEmbedSelected, diff --git a/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx b/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx index 755bef1a2..36cf60830 100644 --- a/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx @@ -28,6 +28,7 @@ export interface TLUiEventMap { 'group-shapes': null 'ungroup-shapes': null 'remove-frame': null + 'fit-frame-to-content': 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 7331c8408..50c1be511 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts @@ -40,6 +40,7 @@ export type TLUiTranslationKey = | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' + | 'action.fit-frame-to-content' | 'action.flip-horizontal' | 'action.flip-vertical' | 'action.flip-horizontal.short' diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts index e1b986460..9363c67f7 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts @@ -40,6 +40,7 @@ export const DEFAULT_TRANSLATION = { 'action.export-as-png': 'Export as PNG', 'action.export-as-svg.short': 'SVG', 'action.export-as-svg': 'Export as SVG', + 'action.fit-frame-to-content': 'Fit to content', 'action.flip-horizontal': 'Flip horizontally', 'action.flip-vertical': 'Flip vertically', 'action.flip-horizontal.short': 'Flip H', diff --git a/packages/tldraw/src/lib/utils/frames/frames.ts b/packages/tldraw/src/lib/utils/frames/frames.ts new file mode 100644 index 000000000..44fba77fc --- /dev/null +++ b/packages/tldraw/src/lib/utils/frames/frames.ts @@ -0,0 +1,101 @@ +import { + Box2d, + Editor, + TLFrameShape, + TLShapeId, + TLShapePartial, + Vec2d, + compact, +} from '@tldraw/editor' + +/** + * Remove a frame. + * + * @param editor - tlraw editor instance. + * @param ids - Ids of the frames you wish to remove. + * + * @public + */ +export function removeFrame(editor: Editor, ids: TLShapeId[]) { + const frames = compact( + ids + .map((id) => editor.getShape(id)) + .filter((f) => f && editor.isShapeOfType(f, 'frame')) + ) + if (!frames.length) return + + const allChildren: TLShapeId[] = [] + editor.batch(() => { + frames.map((frame) => { + const children = editor.getSortedChildIdsForParent(frame.id) + if (children.length) { + editor.reparentShapes(children, frame.parentId, frame.index) + allChildren.push(...children) + } + }) + editor.setSelectedShapes(allChildren) + editor.deleteShapes(ids) + }) +} + +/** @internal */ +export const DEFAULT_FRAME_PADDING = 50 + +/** + * Fit a frame to its content. + * + * @param id - Id of the frame you wish to fit to content. + * @param editor - tlraw editor instance. + * @param opts - Options for fitting the frame. + * + * @public + */ +export function fitFrameToContent(editor: Editor, id: TLShapeId, opts = {} as { padding: number }) { + const frame = editor.getShape(id) + if (!frame) return + + const childIds = editor.getSortedChildIdsForParent(frame.id) + const children = compact(childIds.map((id) => editor.getShape(id))) + if (!children.length) return + + const bounds = Box2d.FromPoints( + children.flatMap((shape) => { + const geometry = editor.getShapeGeometry(shape.id) + return editor.getShapeLocalTransform(shape)!.applyToPoints(geometry.vertices) + }) + ) + + const { padding = DEFAULT_FRAME_PADDING } = opts + const w = bounds.w + 2 * padding + const h = bounds.h + 2 * padding + const dx = padding - bounds.minX + const dy = padding - bounds.minY + // The shapes already perfectly fit the frame. + if (dx === 0 && dy === 0 && frame.props.w === w && frame.props.h === h) return + + const diff = new Vec2d(dx, dy).rot(frame.rotation) + editor.batch(() => { + const changes: TLShapePartial[] = childIds.map((child) => { + const shape = editor.getShape(child)! + return { + id: shape.id, + type: shape.type, + x: shape.x + dx, + y: shape.y + dy, + } + }) + + changes.push({ + id: frame.id, + type: frame.type, + x: frame.x - diff.x, + y: frame.y - diff.y, + props: { + w, + h, + }, + }) + + editor.updateShapes(changes) + }) +} diff --git a/packages/tldraw/src/test/frames.test.ts b/packages/tldraw/src/test/frames.test.ts index 9aad52e49..b4d4a71b3 100644 --- a/packages/tldraw/src/test/frames.test.ts +++ b/packages/tldraw/src/test/frames.test.ts @@ -5,6 +5,7 @@ import { TLShapeId, createShapeId, } from '@tldraw/editor' +import { DEFAULT_FRAME_PADDING, fitFrameToContent, removeFrame } from '../lib/utils/frames/frames' import { TestEditor } from './TestEditor' let editor: TestEditor @@ -713,6 +714,70 @@ describe('frame shapes', () => { arrow = editor.getOnlySelectedShape()! as TLArrowShape expect(arrow.props.end).toMatchObject({ boundShapeId: innerBoxId }) }) + + it('correctly fits to its content', () => { + // Create two rects, their bounds are from [100, 100] to [400, 400], + // so the frame that fits them (with 50px offset) should be from [50, 50] to [450, 450]. + const rectAId = createRect({ pos: [100, 100], size: [100, 100] }) + const rectBId = createRect({ pos: [300, 300], size: [100, 100] }) + + // Create the frame that encloses both rects + const frameId = dragCreateFrame({ down: [0, 0], move: [700, 700], up: [700, 700] }) + const frame = editor.getShape(frameId)! as TLFrameShape + + const rectA = editor.getShape(rectAId)! + const rectB = editor.getShape(rectBId)! + expect(rectA.parentId).toBe(frameId) + expect(rectB.parentId).toBe(frameId) + + fitFrameToContent(editor, frame.id) + const newFrame = editor.getShape(frameId)! as TLFrameShape + expect(newFrame.x).toBe(50) + expect(newFrame.y).toBe(50) + expect(newFrame.props.w).toBe(400) + expect(newFrame.props.h).toBe(400) + + const newRectA = editor.getShape(rectAId)! + const newRectB = editor.getShape(rectBId)! + // Rect positions should change by 50px since the frame moved + // This keeps them in the same relative position + expect(newRectA.x).toBe(DEFAULT_FRAME_PADDING) + expect(newRectA.y).toBe(DEFAULT_FRAME_PADDING) + expect(newRectB.x).toBe(250) + expect(newRectB.y).toBe(250) + }) + + it('uses padding option', () => { + // Create two rects, their bounds are from [100, 100] to [400, 400], + // so the frame that fits them (with 50px offset) should be from [50, 50] to [450, 450]. + const rectAId = createRect({ pos: [100, 100], size: [100, 100] }) + const rectBId = createRect({ pos: [300, 300], size: [100, 100] }) + + // Create the frame that encloses both rects + const frameId = dragCreateFrame({ down: [0, 0], move: [700, 700], up: [700, 700] }) + const frame = editor.getShape(frameId)! as TLFrameShape + + const rectA = editor.getShape(rectAId)! + const rectB = editor.getShape(rectBId)! + expect(rectA.parentId).toBe(frameId) + expect(rectB.parentId).toBe(frameId) + + fitFrameToContent(editor, frame.id, { padding: 100 }) + const newFrame = editor.getShape(frameId)! as TLFrameShape + expect(newFrame.x).toBe(0) + expect(newFrame.y).toBe(0) + expect(newFrame.props.w).toBe(500) + expect(newFrame.props.h).toBe(500) + + const newRectA = editor.getShape(rectAId)! + const newRectB = editor.getShape(rectBId)! + + // frame is at 0,0 so positions should be the same for this test + expect(newRectA.x).toBe(100) + expect(newRectA.y).toBe(100) + expect(newRectB.x).toBe(300) + expect(newRectB.y).toBe(300) + }) }) test('arrows bound to a shape within a group within a frame are reparented if the group is moved outside of the frame', () => { @@ -863,14 +928,14 @@ describe('When deleting/removing a frame', () => { 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]) + removeFrame(editor, [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]) + removeFrame(editor, [frame1Id]) expect(editor.getShape(rectId)?.parentId).toBe(frame2Id) }) })