From 5e54fa368815b2c66b813dc38c105753bed5ca17 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 26 Feb 2024 17:33:15 +0000 Subject: [PATCH] alex/accoutrements: accoutrements --- .../speech-bubble/CustomShapeWithHandles.tsx | 10 +- .../SpeechBubble/SpeechBubbleAccoutrement.tsx | 16 ++ .../SpeechBubble/SpeechBubbleHandle.tsx | 9 +- .../SpeechBubble/ui-overrides.tsx | 2 - packages/editor/api-report.md | 16 ++ packages/editor/api/api.json | 218 ++++++++++++++++++ packages/editor/src/index.ts | 1 + packages/editor/src/lib/TldrawEditor.tsx | 88 ++++++- .../editor/src/lib/config/Accotrements.ts | 13 ++ 9 files changed, 353 insertions(+), 20 deletions(-) create mode 100644 apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleAccoutrement.tsx create mode 100644 packages/editor/src/lib/config/Accotrements.ts diff --git a/apps/examples/src/examples/speech-bubble/CustomShapeWithHandles.tsx b/apps/examples/src/examples/speech-bubble/CustomShapeWithHandles.tsx index a658cb1ede2..138261f9eec 100644 --- a/apps/examples/src/examples/speech-bubble/CustomShapeWithHandles.tsx +++ b/apps/examples/src/examples/speech-bubble/CustomShapeWithHandles.tsx @@ -1,23 +1,17 @@ import { Tldraw } from '@tldraw/tldraw' import '@tldraw/tldraw/tldraw.css' -import { SpeechBubbleTool } from './SpeechBubble/SpeechBubbleTool' -import { SpeechBubbleUtil } from './SpeechBubble/SpeechBubbleUtil' +import { SpeechBubbleAccoutrement } from './SpeechBubble/SpeechBubbleAccoutrement' import { components, customAssetUrls, uiOverrides } from './SpeechBubble/ui-overrides' import './customhandles.css' // There's a guide at the bottom of this file! -// [1] -const shapeUtils = [SpeechBubbleUtil] -const tools = [SpeechBubbleTool] - // [2] export default function CustomShapeWithHandles() { return (
{ + editor.root.find('select')?.addChild(DraggingSpeechBubble) + }, + shapeUtils: [SpeechBubbleUtil], + tools: [SpeechBubbleTool], + components: { + InFrontOfTheCanvas: () => , + }, +} diff --git a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleHandle.tsx b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleHandle.tsx index e30fdc09247..6e94827f205 100644 --- a/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleHandle.tsx +++ b/apps/examples/src/examples/speech-bubble/SpeechBubble/SpeechBubbleHandle.tsx @@ -1,14 +1,9 @@ import { HandleControl, StateNode, Vec, track, useEditor } from '@tldraw/tldraw' -import { useEffect } from 'react' import { SpeechBubbleShape } from './SpeechBubbleUtil' export const SpeechBubbleHandle = track(function SpeechBubbleHandle() { const editor = useEditor() - useEffect(() => { - editor.root.find('select')!.addChild(DraggingSpeechBubble) - }, [editor]) - if (!editor.isInAny('select.idle')) { return null } @@ -41,13 +36,15 @@ export const SpeechBubbleHandle = track(function SpeechBubbleHandle() { // - tool masks (what even is this?) // - snapping/modifiers // - triggering updates on key presses? +// - pointer capture // // drawbacks: // - need to manually handle cancellation, undo-redo, etc // - kinda funky with typescript to have properties on the state node // - how to pass data into this? // - feels pretty unfamiliar to devs - why do i have to do something special to add this to the state tree? -class DraggingSpeechBubble extends StateNode { +// - gesture recognition (can't transition this into a pinch because it never reached our state chart) +export class DraggingSpeechBubble extends StateNode { override id = 'draggingSpeechBubble' initialShape!: SpeechBubbleShape diff --git a/apps/examples/src/examples/speech-bubble/SpeechBubble/ui-overrides.tsx b/apps/examples/src/examples/speech-bubble/SpeechBubble/ui-overrides.tsx index 8eeba912d36..38231fa6b15 100644 --- a/apps/examples/src/examples/speech-bubble/SpeechBubble/ui-overrides.tsx +++ b/apps/examples/src/examples/speech-bubble/SpeechBubble/ui-overrides.tsx @@ -8,7 +8,6 @@ import { toolbarItem, useTools, } from '@tldraw/tldraw' -import { SpeechBubbleHandle } from './SpeechBubbleHandle' // There's a guide at the bottom of this file! @@ -50,7 +49,6 @@ export const components: TLComponents = { ) }, - InFrontOfTheCanvas: () => , } /* diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index c21defedf73..5d524dec5fc 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -76,6 +76,20 @@ import { useValue } from '@tldraw/state'; import { VecModel } from '@tldraw/tlschema'; import { whyAmIRunning } from '@tldraw/state'; +// @public (undocumented) +export interface Accoutrement { + // (undocumented) + components?: Pick; + // (undocumented) + id: string; + // (undocumented) + onMount?: TLOnMountHandler; + // (undocumented) + shapeUtils?: readonly TLAnyShapeUtilConstructor[]; + // (undocumented) + tools?: readonly TLStateNodeConstructor[]; +} + // @public export function angleDistance(fromAngle: number, toAngle: number, direction: number): number; @@ -2031,6 +2045,8 @@ export const TldrawEditor: React_2.NamedExoticComponent; // @public export interface TldrawEditorBaseProps { + // (undocumented) + accoutrements?: Accoutrement[]; autoFocus?: boolean; children?: any; className?: string; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 56f32181e11..9e37cc54a90 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -172,6 +172,192 @@ "name": "", "preserveMemberOrder": false, "members": [ + { + "kind": "Interface", + "canonicalReference": "@tldraw/editor!Accoutrement:interface", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export interface Accoutrement " + } + ], + "fileUrlPath": "packages/editor/src/lib/config/Accotrements.ts", + "releaseTag": "Public", + "name": "Accoutrement", + "preserveMemberOrder": false, + "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!Accoutrement#components:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "components?: " + }, + { + "kind": "Reference", + "text": "Pick", + "canonicalReference": "!Pick:type" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "TLEditorComponents", + "canonicalReference": "@tldraw/editor!TLEditorComponents:type" + }, + { + "kind": "Content", + "text": ", 'InFrontOfTheCanvas' | 'OnTheCanvas'>" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "components", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 5 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!Accoutrement#id:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "id: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "id", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!Accoutrement#onMount:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "onMount?: " + }, + { + "kind": "Reference", + "text": "TLOnMountHandler", + "canonicalReference": "@tldraw/editor!TLOnMountHandler:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "onMount", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!Accoutrement#shapeUtils:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "shapeUtils?: " + }, + { + "kind": "Content", + "text": "readonly " + }, + { + "kind": "Reference", + "text": "TLAnyShapeUtilConstructor", + "canonicalReference": "@tldraw/editor!TLAnyShapeUtilConstructor:type" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "shapeUtils", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!Accoutrement#tools:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "tools?: " + }, + { + "kind": "Content", + "text": "readonly " + }, + { + "kind": "Reference", + "text": "TLStateNodeConstructor", + "canonicalReference": "@tldraw/editor!TLStateNodeConstructor:interface" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "tools", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 4 + } + } + ], + "extendsTokenRanges": [] + }, { "kind": "Function", "canonicalReference": "@tldraw/editor!angleDistance:function(1)", @@ -36333,6 +36519,38 @@ "name": "TldrawEditorBaseProps", "preserveMemberOrder": false, "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!TldrawEditorBaseProps#accoutrements:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "accoutrements?: " + }, + { + "kind": "Reference", + "text": "Accoutrement", + "canonicalReference": "@tldraw/editor!Accoutrement:interface" + }, + { + "kind": "Content", + "text": "[]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "accoutrements", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/editor!TldrawEditorBaseProps#autoFocus:member", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 74fce6444fd..3ba116ffc1f 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -89,6 +89,7 @@ export { } from './lib/components/default-components/DefaultSnapIndictor' export { DefaultSpinner } from './lib/components/default-components/DefaultSpinner' export { DefaultSvgDefs } from './lib/components/default-components/DefaultSvgDefs' +export { type Accoutrement } from './lib/config/Accotrements' export { TAB_ID, createSessionStateSnapshotSignal, diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index fff13cf476b..dee1718b7eb 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -2,6 +2,7 @@ import { SerializedStore, Store, StoreSnapshot } from '@tldraw/store' import { TLRecord, TLStore } from '@tldraw/tlschema' import { Required, annotateError } from '@tldraw/utils' import React, { + ReactElement, memo, useCallback, useLayoutEffect, @@ -10,10 +11,12 @@ import React, { useSyncExternalStore, } from 'react' +import { EMPTY_ARRAY } from '@tldraw/state' import classNames from 'classnames' import { OptionalErrorBoundary } from './components/ErrorBoundary' import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback' import { DefaultLoadingScreen } from './components/default-components/DefaultLoadingScreen' +import { Accoutrement } from './config/Accotrements' import { TLUser, createTLUser } from './config/createTLUser' import { TLAnyShapeUtilConstructor } from './config/defaultShapes' import { Editor } from './editor/Editor' @@ -30,6 +33,7 @@ import { import { useEvent } from './hooks/useEvent' import { useFocusEvents } from './hooks/useFocusEvents' import { useForceUpdate } from './hooks/useForceUpdate' +import { useShallowArrayIdentity, useShallowObjectIdentity } from './hooks/useIdentity' import { useLocalStore } from './hooks/useLocalStore' import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix' import { useZoomCss } from './hooks/useZoomCss' @@ -111,6 +115,8 @@ export interface TldrawEditorBaseProps { * Whether to infer dark mode from the user's OS. Defaults to false. */ inferDarkMode?: boolean + + accoutrements?: Accoutrement[] } /** @@ -137,24 +143,98 @@ const EMPTY_TOOLS_ARRAY = [] as const /** @public */ export const TldrawEditor = memo(function TldrawEditor({ store, - components, + components: _components, className, user: _user, + accoutrements: _accoutrements, + onMount, ...rest }: TldrawEditorProps) { const [container, setContainer] = React.useState(null) const user = useMemo(() => _user ?? createTLUser(), [_user]) const ErrorFallback = - components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback + _components?.ErrorFallback === undefined ? DefaultErrorFallback : _components?.ErrorFallback // apply defaults. if you're using the bare @tldraw/editor package, we // default these to the "tldraw zero" configuration. We have different // defaults applied in @tldraw/tldraw. + + const accoutrements = useShallowArrayIdentity(_accoutrements ?? EMPTY_ARRAY) + + const rawShapeUtils = useShallowArrayIdentity(rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY) + const shapeUtils = useMemo( + () => [ + ...rawShapeUtils, + ...accoutrements.flatMap((accoutrement) => accoutrement.shapeUtils ?? []), + ], + [accoutrements, rawShapeUtils] + ) + + const rawTools = useShallowArrayIdentity(rest.tools ?? EMPTY_TOOLS_ARRAY) + const tools = useMemo( + () => [...rawTools, ...accoutrements.flatMap((accoutrement) => accoutrement.tools ?? [])], + [accoutrements, rawTools] + ) + + const accoutrementOnMount = useCallback( + (editor: Editor) => { + const disposes: (() => void)[] = [] + + if (onMount) { + const dispose = onMount(editor) + if (typeof dispose === 'function') { + disposes.push(dispose) + } + } + + for (const accoutrement of accoutrements) { + if (!accoutrement.onMount) continue + const dispose = accoutrement.onMount(editor) + if (typeof dispose === 'function') { + disposes.push(dispose) + } + } + return () => { + for (const dispose of disposes) { + dispose() + } + } + }, + [accoutrements, onMount] + ) + + const rawComponents = useShallowObjectIdentity(_components ?? {}) + const components = useMemo(() => { + const onTheCanvasChildren: ReactElement[] = [] + const inFrontOfTheCanvasChildren: ReactElement[] = [] + + if (rawComponents.OnTheCanvas) { + onTheCanvasChildren.push() + } + if (rawComponents.InFrontOfTheCanvas) { + inFrontOfTheCanvasChildren.push() + } + + for (const { id, components } of accoutrements) { + if (!components) continue + const { OnTheCanvas, InFrontOfTheCanvas } = components + if (OnTheCanvas) onTheCanvasChildren.push() + if (InFrontOfTheCanvas) inFrontOfTheCanvasChildren.push() + } + + return { + ...rawComponents, + OnTheCanvas: () => <>{onTheCanvasChildren}, + InFrontOfTheCanvas: () => <>{inFrontOfTheCanvasChildren}, + } + }, [accoutrements, rawComponents]) + const withDefaults = { ...rest, - shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY, - tools: rest.tools ?? EMPTY_TOOLS_ARRAY, + shapeUtils: shapeUtils, + tools: tools, + onMount: accoutrementOnMount, components, } diff --git a/packages/editor/src/lib/config/Accotrements.ts b/packages/editor/src/lib/config/Accotrements.ts new file mode 100644 index 00000000000..14df85f4448 --- /dev/null +++ b/packages/editor/src/lib/config/Accotrements.ts @@ -0,0 +1,13 @@ +import { TLOnMountHandler } from '../TldrawEditor' +import { TLStateNodeConstructor } from '../editor/tools/StateNode' +import { TLEditorComponents } from '../hooks/useEditorComponents' +import { TLAnyShapeUtilConstructor } from './defaultShapes' + +/** @public */ +export interface Accoutrement { + id: string + shapeUtils?: readonly TLAnyShapeUtilConstructor[] + tools?: readonly TLStateNodeConstructor[] + onMount?: TLOnMountHandler + components?: Pick +}