[feature] ui events (#1326)

This PR updates the editor events:
- adds types to the events emitted by the app (by `app.emit`)
- removes a few events emitted by the app (e.g. `move-to-page`,
`change-camera`)
- adds `onEvent` prop to the <TldrawUi> / <Tldraw> components
- call the `onEvent` when actions occur or tools are selected
- does some superficial cleanup on editor app APIs

### Release Note

- Fix layout bug in error dialog
- (ui) Add `TLEventMap` for types emitted from editor app
- (editor) Update `crash` event emitted from editor app to include error
- (editor) Update `change-history` event emitted from editor app
- (editor) Remove `change-camera` event from editor app
- (editor) Remove `move-to-page` event from editor app
- (ui) Add `onEvent` prop and events to <Tldraw> / <TldrawUi>
- (editor) Replace `app.openMenus` plain Set with computed value
- (editor) Add `addOpenMenu` method
- (editor) Add `removeOpenMenu` method
- (editor) Add `setFocusMode` method 
- (editor) Add `setToolLocked` method  
- (editor) Add `setSnapMode` method 
- (editor) Add `isSnapMode` method 
- (editor) Update `setGridMode` method return type to editor app
- (editor) Update `setReadOnly` method return type to editor app
- (editor) Update `setPenMode` method return type to editor app
- (editor) Update `selectNone` method return type to editor app
- (editor) Rename `backToContent` to `zoomToContent`
- (editor) Remove `TLReorderOperation` type

---------

Co-authored-by: Orange Mug <orangemug@users.noreply.github.com>
pull/1355/head
Steve Ruiz 2023-05-11 23:14:58 +01:00 zatwierdzone przez GitHub
rodzic 5061240912
commit 3437ca89d9
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
50 zmienionych plików z 935 dodań i 344 usunięć

21
.ignore 100644
Wyświetl plik

@ -0,0 +1,21 @@
dist
.tsbuild-dev
.tsbuild-pub
.tsbuild
node\*modules
*.d.ts
*.md
**/_archive
**/*.tsbuildinfo
yarn.lock
**/*.tldr
**/*.d.ts
**/*.d.ts.map
**/*.js.map
**/*.css.map
apps/example/www/index.css
**/dist/*
*.cjs
bublic/packages/tldraw/ui.css
bublic/packages/tldraw/editor.css

Wyświetl plik

@ -31,18 +31,17 @@
"build": "vite build", "build": "vite build",
"lint": "yarn run -T tsx ../../scripts/lint.ts" "lint": "yarn run -T tsx ../../scripts/lint.ts"
}, },
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.21.0",
"lazyrepo": "0.0.0-alpha.26",
"vite": "^4.3.4"
},
"dependencies": { "dependencies": {
"@babel/plugin-proposal-decorators": "^7.21.0",
"@tldraw/assets": "workspace:*", "@tldraw/assets": "workspace:*",
"@tldraw/tldraw": "workspace:*", "@tldraw/tldraw": "workspace:*",
"@vercel/analytics": "^1.0.1",
"lazyrepo": "0.0.0-alpha.26",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"signia": "0.1.4", "signia": "0.1.4",
"signia-react": "0.1.4" "signia-react": "0.1.4",
"vite": "^4.3.4"
} }
} }

Wyświetl plik

@ -0,0 +1,95 @@
import { App, TLEventMapHandler, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { TLUiEventHandler } from '@tldraw/ui/src/lib/hooks/useEventsProvider'
import { useCallback, useEffect, useState } from 'react'
export default function Example() {
const [app, setApp] = useState<App>()
const setAppToState = useCallback((app: App) => {
setApp(app)
}, [])
const [uiEvents, setUiEvents] = useState<string[]>([])
const handleEvent = useCallback<TLUiEventHandler>((source, name, data) => {
setUiEvents((events) => [
data ? `${source} ${name} ${JSON.stringify(data)}` : `${source} ${name}`,
...events,
])
}, [])
useEffect(() => {
if (!app) return
function logChangeEvent(eventName: string) {
setUiEvents((events) => [eventName, ...events])
}
// This is the fire hose, it will be called at the end of every transaction
const handleChangeEvent: TLEventMapHandler<'change'> = (change) => {
if (change.source === 'user') {
// Added
for (const record of Object.values(change.changes.added)) {
if (record.typeName === 'shape') {
logChangeEvent(`created shape (${record.type})`)
}
}
// Updated
for (const [from, to] of Object.values(change.changes.updated)) {
if (
from.typeName === 'instance' &&
to.typeName === 'instance' &&
from.currentPageId !== to.currentPageId
) {
logChangeEvent(`changed page (${from.currentPageId}, ${to.currentPageId})`)
}
}
// Removed
for (const record of Object.values(change.changes.removed)) {
if (record.typeName === 'shape') {
logChangeEvent(`deleted shape (${record.type})`)
}
}
}
}
app.on('change', handleChangeEvent)
return () => {
app.off('change', handleChangeEvent)
}
}, [app])
return (
<div style={{ display: 'flex' }}>
<div style={{ width: '60vw', height: '100vh' }}>
<Tldraw autoFocus onMount={setAppToState} onEvent={handleEvent} />
</div>
<div>
<div
style={{
width: '40vw',
height: '100vh',
padding: 8,
background: '#eee',
border: 'none',
fontFamily: 'monospace',
fontSize: 12,
borderLeft: 'solid 2px #333',
display: 'flex',
flexDirection: 'column-reverse',
overflow: 'auto',
}}
>
{uiEvents.map((t, i) => (
<div key={i}>{t}</div>
))}
</div>
</div>
</div>
)
}

Wyświetl plik

@ -11,6 +11,7 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom'
import ExampleBasic from './1-basic/BasicExample' import ExampleBasic from './1-basic/BasicExample'
import CustomComponentsExample from './10-custom-components/CustomComponentsExample' import CustomComponentsExample from './10-custom-components/CustomComponentsExample'
import UserPresenceExample from './11-user-presence/UserPresenceExample' import UserPresenceExample from './11-user-presence/UserPresenceExample'
import EventsExample from './12-events/EventsExample'
import ExampleApi from './2-api/APIExample' import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample' import CustomConfigExample from './3-custom-config/CustomConfigExample'
import CustomUiExample from './4-custom-ui/CustomUiExample' import CustomUiExample from './4-custom-ui/CustomUiExample'
@ -71,6 +72,10 @@ export const allExamples: Example[] = [
path: '/custom-components', path: '/custom-components',
element: <CustomComponentsExample />, element: <CustomComponentsExample />,
}, },
{
path: '/events',
element: <EventsExample />,
},
{ {
path: '/user-presence', path: '/user-presence',
element: <UserPresenceExample />, element: <UserPresenceExample />,

Wyświetl plik

@ -7,6 +7,7 @@
/// <reference types="react" /> /// <reference types="react" />
import { Atom } from 'signia'; import { Atom } from 'signia';
import { BaseRecord } from '@tldraw/tlstore';
import { Box2d } from '@tldraw/primitives'; import { Box2d } from '@tldraw/primitives';
import { Box2dModel } from '@tldraw/tlschema'; import { Box2dModel } from '@tldraw/tlschema';
import { Computed } from 'signia'; import { Computed } from 'signia';
@ -120,8 +121,9 @@ export type AnimationOptions = Partial<{
}>; }>;
// @public (undocumented) // @public (undocumented)
export class App extends EventEmitter { export class App extends EventEmitter<TLEventMap> {
constructor({ config, store, getContainer }: AppOptions); constructor({ config, store, getContainer }: AppOptions);
addOpenMenu: (id: string) => this;
alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this; alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this;
get allShapesCommonBounds(): Box2d | null; get allShapesCommonBounds(): Box2d | null;
animateCamera(x: number, y: number, z?: number, opts?: AnimationOptions): this; animateCamera(x: number, y: number, z?: number, opts?: AnimationOptions): this;
@ -193,6 +195,7 @@ export class App extends EventEmitter {
// (undocumented) // (undocumented)
get cursor(): TLCursor; get cursor(): TLCursor;
deleteAssets(ids: TLAssetId[]): this; deleteAssets(ids: TLAssetId[]): this;
deleteOpenMenu: (id: string) => this;
deletePage(id: TLPageId): void; deletePage(id: TLPageId): void;
deleteShapes(ids?: TLShapeId[]): this; deleteShapes(ids?: TLShapeId[]): this;
deselect(...ids: TLShapeId[]): this; deselect(...ids: TLShapeId[]): this;
@ -326,8 +329,12 @@ export class App extends EventEmitter {
readonly isChromeForIos: boolean; readonly isChromeForIos: boolean;
get isCoarsePointer(): boolean; get isCoarsePointer(): boolean;
set isCoarsePointer(v: boolean); set isCoarsePointer(v: boolean);
// (undocumented)
get isDarkMode(): boolean;
get isFocused(): boolean; get isFocused(): boolean;
// (undocumented) // (undocumented)
get isFocusMode(): boolean;
// (undocumented)
get isGridMode(): boolean; get isGridMode(): boolean;
isIn(path: string): boolean; isIn(path: string): boolean;
isInAny(...paths: string[]): boolean; isInAny(...paths: string[]): boolean;
@ -342,6 +349,10 @@ export class App extends EventEmitter {
isSelected(id: TLShapeId): boolean; isSelected(id: TLShapeId): boolean;
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean; isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
isShapeInViewport(id: TLShapeId): boolean; isShapeInViewport(id: TLShapeId): boolean;
// (undocumented)
get isSnapMode(): boolean;
// (undocumented)
get isToolLocked(): boolean;
isWithinSelection(id: TLShapeId): boolean; isWithinSelection(id: TLShapeId): boolean;
// (undocumented) // (undocumented)
lockShapes(_ids?: TLShapeId[]): this; lockShapes(_ids?: TLShapeId[]): this;
@ -355,7 +366,7 @@ export class App extends EventEmitter {
description: string; description: string;
}>; }>;
get onlySelectedShape(): TLBaseShape<any, any> | null; get onlySelectedShape(): TLBaseShape<any, any> | null;
openMenus: Set<string>; get openMenus(): string[];
packShapes(ids?: TLShapeId[], padding?: number): this; packShapes(ids?: TLShapeId[], padding?: number): this;
get pages(): TLPage[]; get pages(): TLPage[];
get pageState(): TLInstancePageState; get pageState(): TLInstancePageState;
@ -386,7 +397,7 @@ export class App extends EventEmitter {
isCulled: boolean; isCulled: boolean;
isInViewport: boolean; isInViewport: boolean;
}[]; }[];
reorderShapes(operation: TLReorderOperation, ids: TLShapeId[]): this; reorderShapes(operation: 'backward' | 'forward' | 'toBack' | 'toFront', ids: TLShapeId[]): this;
reparentShapesById(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this; reparentShapesById(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
// (undocumented) // (undocumented)
replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]): void; replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]): void;
@ -431,23 +442,29 @@ export class App extends EventEmitter {
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: ViewportOptions): this; setCurrentPageId(pageId: TLPageId, { stopFollowing }?: ViewportOptions): this;
setCursor(cursor: Partial<TLCursor>): this; setCursor(cursor: Partial<TLCursor>): this;
// (undocumented) // (undocumented)
setDarkMode(isDarkMode: boolean): void; setDarkMode(isDarkMode: boolean): this;
setEditingId(id: null | TLShapeId): this; setEditingId(id: null | TLShapeId): this;
setErasingIds(ids?: TLShapeId[]): this; setErasingIds(ids?: TLShapeId[]): this;
setFocusLayer(next: null | TLShapeId): this; setFocusLayer(next: null | TLShapeId): this;
// (undocumented) // (undocumented)
setGridMode(isGridMode: boolean): void; setFocusMode(isFocusMode: boolean): this;
// (undocumented)
setGridMode(isGridMode: boolean): this;
setHintingIds(ids: TLShapeId[]): this; setHintingIds(ids: TLShapeId[]): this;
setHoveredId(id?: null | TLShapeId): this; setHoveredId(id?: null | TLShapeId): this;
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void; setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
// (undocumented) // (undocumented)
setPenMode(isPenMode: boolean): void; setPenMode(isPenMode: boolean): this;
setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this; setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this;
// (undocumented) // (undocumented)
setReadOnly(isReadOnly: boolean): void; setReadOnly(isReadOnly: boolean): this;
setScribble(scribble?: null | TLScribble): this; setScribble(scribble?: null | TLScribble): this;
setSelectedIds(ids: TLShapeId[], squashing?: boolean): this; setSelectedIds(ids: TLShapeId[], squashing?: boolean): this;
setSelectedTool(id: string, info?: {}): this; setSelectedTool(id: string, info?: {}): this;
// (undocumented)
setSnapMode(isSnapMode: boolean): this;
// (undocumented)
setToolLocked(isToolLocked: boolean): this;
setZoomBrush(zoomBrush?: Box2dModel | null): this; setZoomBrush(zoomBrush?: Box2dModel | null): this;
get shapeIds(): Set<TLShapeId>; get shapeIds(): Set<TLShapeId>;
get shapesArray(): TLShape[]; get shapesArray(): TLShape[];
@ -507,6 +524,7 @@ export class App extends EventEmitter {
get zoomLevel(): number; get zoomLevel(): number;
zoomOut(point?: Vec2d, opts?: AnimationOptions): this; zoomOut(point?: Vec2d, opts?: AnimationOptions): this;
zoomToBounds(x: number, y: number, width: number, height: number, targetZoom?: number, opts?: AnimationOptions): this; zoomToBounds(x: number, y: number, width: number, height: number, targetZoom?: number, opts?: AnimationOptions): this;
zoomToContent(): this;
zoomToFit(opts?: AnimationOptions): this; zoomToFit(opts?: AnimationOptions): this;
zoomToSelection(opts?: AnimationOptions): this; zoomToSelection(opts?: AnimationOptions): this;
} }
@ -1729,7 +1747,7 @@ export type TLCancelEventInfo = {
}; };
// @public (undocumented) // @public (undocumented)
export type TLChange = HistoryEntry<any>; export type TLChange<T extends BaseRecord<any> = any> = HistoryEntry<T>;
// @public (undocumented) // @public (undocumented)
export type TLClickEvent = (info: TLClickEventInfo) => void; export type TLClickEvent = (info: TLClickEventInfo) => void;
@ -1990,6 +2008,48 @@ export interface TLEventHandlers {
// @public (undocumented) // @public (undocumented)
export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEventInfo | TLInterruptEventInfo | TLKeyboardEventInfo | TLPinchEventInfo | TLPointerEventInfo | TLWheelEventInfo; export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEventInfo | TLInterruptEventInfo | TLKeyboardEventInfo | TLPinchEventInfo | TLPointerEventInfo | TLWheelEventInfo;
// @public (undocumented)
export interface TLEventMap {
// (undocumented)
'change-history': [{
reason: 'bail';
markId?: string;
} | {
reason: 'push' | 'redo' | 'undo';
}];
// (undocumented)
'mark-history': [{
id: string;
}];
// (undocumented)
'max-shapes': [{
name: string;
pageId: TLPageId;
count: number;
}];
// (undocumented)
'stop-camera-animation': [];
// (undocumented)
'stop-following': [];
// (undocumented)
change: [TLChange<TLRecord>];
// (undocumented)
crash: [{
error: unknown;
}];
// (undocumented)
event: [TLEventInfo];
// (undocumented)
mount: [];
// (undocumented)
tick: [number];
// (undocumented)
update: [];
}
// @public (undocumented)
export type TLEventMapHandler<T extends keyof TLEventMap> = (...args: TLEventMap[T]) => void;
// @public (undocumented) // @public (undocumented)
export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName; export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName;
@ -2403,9 +2463,6 @@ export type TLPointerEventTarget = {
shape: TLShape; shape: TLShape;
}; };
// @public (undocumented)
export type TLReorderOperation = 'backward' | 'forward' | 'toBack' | 'toFront';
// @public (undocumented) // @public (undocumented)
export type TLResizeHandle = SelectionCorner | SelectionEdge; export type TLResizeHandle = SelectionCorner | SelectionEdge;

Wyświetl plik

@ -1392,6 +1392,7 @@ input,
.tl-error-boundary__overlay { .tl-error-boundary__overlay {
position: absolute; position: absolute;
inset: 0px; inset: 0px;
z-index: 500;
background-color: var(--color-overlay); background-color: var(--color-overlay);
} }
@ -1435,6 +1436,7 @@ it from receiving any pointer events or affecting the cursor. */
flex-direction: column; flex-direction: column;
gap: var(--space-5); gap: var(--space-5);
overflow: auto; overflow: auto;
z-index: 600;
} }
.tl-error-boundary__content__expanded { .tl-error-boundary__content__expanded {
width: 600px; width: 600px;

Wyświetl plik

@ -66,6 +66,7 @@ export { TLVideoShapeDef, TLVideoUtil } from './lib/app/shapeutils/TLVideoUtil/T
export { StateNode, type StateNodeConstructor } from './lib/app/statechart/StateNode' export { StateNode, type StateNodeConstructor } from './lib/app/statechart/StateNode'
export { TLBoxTool, type TLBoxLike } from './lib/app/statechart/TLBoxTool/TLBoxTool' export { TLBoxTool, type TLBoxLike } from './lib/app/statechart/TLBoxTool/TLBoxTool'
export { type ClipboardPayload, type TLClipboardModel } from './lib/app/types/clipboard-types' export { type ClipboardPayload, type TLClipboardModel } from './lib/app/types/clipboard-types'
export { type TLEventMap, type TLEventMapHandler } from './lib/app/types/emit-types'
export { export {
EVENT_NAME_MAP, EVENT_NAME_MAP,
type TLBaseEventInfo, type TLBaseEventInfo,
@ -106,7 +107,6 @@ export {
type TLMark, type TLMark,
} from './lib/app/types/history-types' } from './lib/app/types/history-types'
export { type RequiredKeys, type TLEasingType } from './lib/app/types/misc-types' export { type RequiredKeys, type TLEasingType } from './lib/app/types/misc-types'
export { type TLReorderOperation } from './lib/app/types/reorder-types'
export { type TLResizeHandle, type TLSelectionHandle } from './lib/app/types/selection-types' export { type TLResizeHandle, type TLSelectionHandle } from './lib/app/types/selection-types'
export { export {
defaultEditorAssetUrls, defaultEditorAssetUrls,

Wyświetl plik

@ -237,7 +237,11 @@ function TldrawEditorAfterLoading({
} }
}, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl]) }, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl])
const onMountEvent = useEvent((app: App) => onMount?.(app)) const onMountEvent = useEvent((app: App) => {
onMount?.(app)
app.emit('mount')
})
React.useEffect(() => { React.useEffect(() => {
if (app) { if (app) {
// Set the initial theme state. // Set the initial theme state.

Wyświetl plik

@ -55,7 +55,7 @@ import {
TLVideoAsset, TLVideoAsset,
Vec2dModel, Vec2dModel,
} from '@tldraw/tlschema' } from '@tldraw/tlschema'
import { ComputedCache, HistoryEntry } from '@tldraw/tlstore' import { BaseRecord, ComputedCache, HistoryEntry } from '@tldraw/tlstore'
import { annotateError, compact, dedupe, deepCopy, partition, structuredClone } from '@tldraw/utils' import { annotateError, compact, dedupe, deepCopy, partition, structuredClone } from '@tldraw/utils'
import { EventEmitter } from 'eventemitter3' import { EventEmitter } from 'eventemitter3'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
@ -126,13 +126,13 @@ import { TLTextShapeDef } from './shapeutils/TLTextUtil/TLTextUtil'
import { RootState } from './statechart/RootState' import { RootState } from './statechart/RootState'
import { StateNode } from './statechart/StateNode' import { StateNode } from './statechart/StateNode'
import { TLClipboardModel } from './types/clipboard-types' import { TLClipboardModel } from './types/clipboard-types'
import { TLEventMap } from './types/emit-types'
import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types' import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types'
import { RequiredKeys } from './types/misc-types' import { RequiredKeys } from './types/misc-types'
import { TLReorderOperation } from './types/reorder-types'
import { TLResizeHandle } from './types/selection-types' import { TLResizeHandle } from './types/selection-types'
/** @public */ /** @public */
export type TLChange = HistoryEntry<any> export type TLChange<T extends BaseRecord<any> = any> = HistoryEntry<T>
/** @public */ /** @public */
export type AnimationOptions = Partial<{ export type AnimationOptions = Partial<{
@ -168,7 +168,7 @@ export function isShapeWithHandles(shape: TLShape) {
} }
/** @public */ /** @public */
export class App extends EventEmitter { export class App extends EventEmitter<TLEventMap> {
constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) { constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) {
super() super()
@ -282,14 +282,14 @@ export class App extends EventEmitter {
true true
) )
this.root.enter(undefined, 'initial')
if (this.instanceState.followingUserId) { if (this.instanceState.followingUserId) {
this.stopFollowingUser() this.stopFollowingUser()
} }
this.updateCullingBounds() this.updateCullingBounds()
this.root.enter(undefined, 'initial')
requestAnimationFrame(() => { requestAnimationFrame(() => {
this._tickManager.start() this._tickManager.start()
}) })
@ -533,27 +533,71 @@ export class App extends EventEmitter {
crash(error: unknown) { crash(error: unknown) {
this._crashingError = error this._crashingError = error
this.store.markAsPossiblyCorrupted() this.store.markAsPossiblyCorrupted()
this.emit('crash') this.emit('crash', { error })
} }
get devicePixelRatio() { get devicePixelRatio() {
return this._dprManager.dpr.value return this._dprManager.dpr.value
} }
private _openMenus = atom('open-menus', [] as string[])
/** /**
* A set of strings representing any open menus or modals. * A set of strings representing any open menus. When menus are open,
* certain interactions will behave differently; for example, when a
* draw tool is selected and a menu is open, a pointer-down will not
* create a dot (because the user is probably trying to close the menu)
* however a pointer-down event followed by a drag will begin drawing
* a line (because the user is BOTH trying to close the menu AND start
* drawing a line).
* *
* @public * @public
*/ */
openMenus = new Set<string>() @computed get openMenus(): string[] {
return this._openMenus.value
}
/**
* Add an open menu.
*
* ```ts
* app.addOpenMenu('menu-id')
* ```
* @public
*/
addOpenMenu = (id: string) => {
const menus = new Set(this.openMenus)
if (!menus.has(id)) {
menus.add(id)
this._openMenus.set([...menus])
}
return this
}
/**
* Delete an open menu.
*
* ```ts
* app.deleteOpenMenu('menu-id')
* ```
* @public
*/
deleteOpenMenu = (id: string) => {
const menus = new Set(this.openMenus)
if (menus.has(id)) {
menus.delete(id)
this._openMenus.set([...menus])
}
return this
}
/** /**
* Get whether any menus are open. * Get whether any menus are open.
* *
* @public * @public
*/ */
get isMenuOpen() { @computed get isMenuOpen() {
return this.openMenus.size > 0 return this.openMenus.length > 0
} }
/** @internal */ /** @internal */
@ -1452,6 +1496,50 @@ export class App extends EventEmitter {
return this.store.get(this.userId)! return this.store.get(this.userId)!
} }
get isSnapMode() {
return this.userDocumentSettings.isSnapMode
}
setSnapMode(isSnapMode: boolean) {
if (isSnapMode !== this.isSnapMode) {
this.updateUserDocumentSettings({ isSnapMode }, true)
}
return this
}
get isDarkMode() {
return this.userDocumentSettings.isDarkMode
}
setDarkMode(isDarkMode: boolean) {
if (isDarkMode !== this.isDarkMode) {
this.updateUserDocumentSettings({ isDarkMode }, true)
}
return this
}
get isFocusMode() {
return this.instanceState.isFocusMode
}
setFocusMode(isFocusMode: boolean) {
if (isFocusMode !== this.isFocusMode) {
this.updateInstanceState({ isFocusMode }, true)
}
return this
}
get isToolLocked() {
return this.instanceState.isToolLocked
}
setToolLocked(isToolLocked: boolean) {
if (isToolLocked !== this.isToolLocked) {
this.updateInstanceState({ isToolLocked }, true)
}
return this
}
/** @internal */ /** @internal */
@computed private get _userDocumentSettings() { @computed private get _userDocumentSettings() {
return this.store.query.record('user_document', () => ({ userId: { eq: this.userId } })) return this.store.query.record('user_document', () => ({ userId: { eq: this.userId } }))
@ -1461,28 +1549,30 @@ export class App extends EventEmitter {
return this._userDocumentSettings.value! return this._userDocumentSettings.value!
} }
get isReadOnly() {
return this.userDocumentSettings.isReadOnly
}
get isGridMode() { get isGridMode() {
return this.userDocumentSettings.isGridMode return this.userDocumentSettings.isGridMode
} }
setGridMode(isGridMode: boolean) { setGridMode(isGridMode: boolean): this {
if (isGridMode === this.isGridMode) {
this.updateUserDocumentSettings({ isGridMode }, true) this.updateUserDocumentSettings({ isGridMode }, true)
} }
return this
setDarkMode(isDarkMode: boolean) {
this.updateUserDocumentSettings({ isDarkMode }, true)
} }
setReadOnly(isReadOnly: boolean) { get isReadOnly() {
return this.userDocumentSettings.isReadOnly
}
setReadOnly(isReadOnly: boolean): this {
if (isReadOnly !== this.isReadOnly) {
this.updateUserDocumentSettings({ isReadOnly }, true) this.updateUserDocumentSettings({ isReadOnly }, true)
if (isReadOnly) { if (isReadOnly) {
this.setSelectedTool('hand') this.setSelectedTool('hand')
} }
} }
return this
}
/** @internal */ /** @internal */
private _isPenMode = atom<boolean>('isPenMode', false as any) private _isPenMode = atom<boolean>('isPenMode', false as any)
@ -1494,11 +1584,13 @@ export class App extends EventEmitter {
return this._isPenMode.value return this._isPenMode.value
} }
setPenMode(isPenMode: boolean) { setPenMode(isPenMode: boolean): this {
if (isPenMode) this._touchEventsRemainingBeforeExitingPenMode = 3 if (isPenMode) this._touchEventsRemainingBeforeExitingPenMode = 3
if (isPenMode !== this.isPenMode) {
this._isPenMode.set(isPenMode) this._isPenMode.set(isPenMode)
} }
return this
}
// User / User App State // User / User App State
@ -4410,6 +4502,8 @@ export class App extends EventEmitter {
return { return {
data: { data: {
currentPageId: this.currentPageId,
createdIds: partials.map((p) => p.id),
prevSelectedIds, prevSelectedIds,
partials: partialsToCreate, partials: partialsToCreate,
select, select,
@ -4417,7 +4511,7 @@ export class App extends EventEmitter {
} }
}, },
{ {
do: ({ partials, select }) => { do: ({ createdIds, partials, select }) => {
const { focusLayerId } = this const { focusLayerId } = this
// 1. Parents // 1. Parents
@ -4463,7 +4557,7 @@ export class App extends EventEmitter {
const parentIndices = new Map<string, string>() const parentIndices = new Map<string, string>()
const shapeRecordsTocreate: TLShape[] = [] const shapeRecordsToCreate: TLShape[] = []
for (const partial of partials) { for (const partial of partials) {
const util = this.getShapeUtil(partial as TLShape) const util = this.getShapeUtil(partial as TLShape)
@ -4516,20 +4610,23 @@ export class App extends EventEmitter {
shapeRecordToCreate = next shapeRecordToCreate = next
} }
shapeRecordsTocreate.push(shapeRecordToCreate) shapeRecordsToCreate.push(shapeRecordToCreate)
} }
this.store.put(shapeRecordsTocreate) this.store.put(shapeRecordsToCreate)
// If we're also selecting the newly created shapes, attempt to select all of them; // If we're also selecting the newly created shapes, attempt to select all of them;
// the engine will filter out any shapes that are descendants of other new shapes. // the engine will filter out any shapes that are descendants of other new shapes.
if (select) { if (select) {
const selectedIds = partials.map((partial) => partial.id) this.store.update(this.pageState.id, (state) => ({
this.store.update(this.pageState.id, (state) => ({ ...state, selectedIds })) ...state,
selectedIds: createdIds,
}))
} }
}, },
undo: ({ partials, prevSelectedIds }) => { undo: ({ createdIds, prevSelectedIds }) => {
this.store.remove(partials.map((p) => p.id)) this.store.remove(createdIds)
if (prevSelectedIds) { if (prevSelectedIds) {
this.store.update(this.pageState.id, (state) => ({ this.store.update(this.pageState.id, (state) => ({
@ -4987,6 +5084,7 @@ export class App extends EventEmitter {
undo: ({ newPage, prevPageState, prevTabState, newTabPageState }) => { undo: ({ newPage, prevPageState, prevTabState, newTabPageState }) => {
this.store.put([prevPageState, prevTabState]) this.store.put([prevPageState, prevTabState])
this.store.remove([newTabPageState.id, newPage.id, newTabPageState.cameraId]) this.store.remove([newTabPageState.id, newPage.id, newTabPageState.cameraId])
this.updateCullingBounds() this.updateCullingBounds()
}, },
} }
@ -5271,6 +5369,7 @@ export class App extends EventEmitter {
this.store.put(assets) this.store.put(assets)
}, },
undo: ({ assets }) => { undo: ({ assets }) => {
// todo: should we actually remove assets here? or on cleanup elsewhere?
this.store.remove(assets.map((a) => a.id)) this.store.remove(assets.map((a) => a.id))
}, },
} }
@ -5739,8 +5838,6 @@ export class App extends EventEmitter {
this.centerOnPoint(x, y) this.centerOnPoint(x, y)
}) })
this.emit('moved-to-page', { name: this.currentPage.name, toId: pageId, fromId: currentPageId })
return this return this
} }
@ -5757,9 +5854,10 @@ export class App extends EventEmitter {
* @param ids - The ids to reorder. * @param ids - The ids to reorder.
* @public * @public
*/ */
reorderShapes(operation: TLReorderOperation, ids: TLShapeId[]) { reorderShapes(operation: 'toBack' | 'toFront' | 'forward' | 'backward', ids: TLShapeId[]) {
if (this.isReadOnly) return this if (this.isReadOnly) return this
if (ids.length === 0) return this if (ids.length === 0) return this
// this.emit('reorder-shapes', { pageId: this.currentPageId, ids, operation })
const parents = this.getParentsMappedToChildren(ids) const parents = this.getParentsMappedToChildren(ids)
@ -6980,6 +7078,7 @@ export class App extends EventEmitter {
// page might have no shapes // page might have no shapes
if (ids.length <= 0) return this if (ids.length <= 0) return this
this.setSelectedIds(ids) this.setSelectedIds(ids)
return this return this
} }
@ -7020,10 +7119,11 @@ export class App extends EventEmitter {
* *
* @public * @public
*/ */
selectNone() { selectNone(): this {
if (this.selectedIds.length > 0) { if (this.selectedIds.length > 0) {
this.setSelectedIds([]) this.setSelectedIds([])
} }
return this return this
} }
@ -7040,7 +7140,7 @@ export class App extends EventEmitter {
* @param options - Options for setting the current page. * @param options - Options for setting the current page.
* @public * @public
*/ */
setCurrentPageId(pageId: TLPageId, { stopFollowing = true }: ViewportOptions = {}) { setCurrentPageId(pageId: TLPageId, { stopFollowing = true }: ViewportOptions = {}): this {
this._setCurrentPageId(pageId, { stopFollowing }) this._setCurrentPageId(pageId, { stopFollowing })
return this return this
} }
@ -7059,42 +7159,42 @@ export class App extends EventEmitter {
} }
return { return {
data: { pageId, prev: this.currentPageId }, data: { toId: pageId, fromId: this.currentPageId },
squashing: true, squashing: true,
preservesRedoStack: true, preservesRedoStack: true,
} }
}, },
{ {
do: ({ pageId }) => { do: ({ toId }) => {
if (!this.getPageStateByPageId(pageId)) { if (!this.getPageStateByPageId(toId)) {
const camera = TLCamera.create({}) const camera = TLCamera.create({})
this.store.put([ this.store.put([
camera, camera,
TLInstancePageState.create({ TLInstancePageState.create({
pageId, pageId: toId,
instanceId: this.instanceId, instanceId: this.instanceId,
cameraId: camera.id, cameraId: camera.id,
}), }),
]) ])
} }
this.store.put([{ ...this.instanceState, currentPageId: pageId }]) this.store.put([{ ...this.instanceState, currentPageId: toId }])
this.updateUserPresence({ this.updateUserPresence({
viewportPageBounds: this.viewportPageBounds.toJson(), viewportPageBounds: this.viewportPageBounds.toJson(),
}) })
this.updateCullingBounds() this.updateCullingBounds()
}, },
undo: ({ prev }) => { undo: ({ fromId }) => {
this.store.put([{ ...this.instanceState, currentPageId: prev }]) this.store.put([{ ...this.instanceState, currentPageId: fromId }])
this.updateUserPresence({ this.updateUserPresence({
viewportPageBounds: this.viewportPageBounds.toJson(), viewportPageBounds: this.viewportPageBounds.toJson(),
}) })
this.updateCullingBounds() this.updateCullingBounds()
}, },
squash: ({ prev }, { pageId }) => { squash: ({ fromId }, { toId }) => {
return { pageId, prev } return { toId, fromId }
}, },
} }
) )
@ -7161,7 +7261,7 @@ export class App extends EventEmitter {
* @param id - The id of the page to set as the current page * @param id - The id of the page to set as the current page
* @public * @public
*/ */
setHoveredId(id: TLShapeId | null = null) { setHoveredId(id: TLShapeId | null = null): this {
if (id === this.pageState.hoveredId) return this if (id === this.pageState.hoveredId) return this
this.setInstancePageState({ hoveredId: id }, true) this.setInstancePageState({ hoveredId: id }, true)
@ -7181,7 +7281,7 @@ export class App extends EventEmitter {
* @param ids - The ids of shapes to set as erasing. * @param ids - The ids of shapes to set as erasing.
* @public * @public
*/ */
setErasingIds(ids: TLShapeId[] = []) { setErasingIds(ids: TLShapeId[] = []): this {
const erasingIds = this.erasingIdsSet const erasingIds = this.erasingIdsSet
if (ids.length === erasingIds.size && ids.every((id) => erasingIds.has(id))) return this if (ids.length === erasingIds.size && ids.every((id) => erasingIds.has(id))) return this
@ -7202,7 +7302,7 @@ export class App extends EventEmitter {
* @param cursor - A partial of the cursor object. * @param cursor - A partial of the cursor object.
* @public * @public
*/ */
setCursor(cursor: Partial<TLCursor>) { setCursor(cursor: Partial<TLCursor>): this {
const current = this.cursor const current = this.cursor
const next = { const next = {
...current, ...current,
@ -7236,7 +7336,7 @@ export class App extends EventEmitter {
* @param scribble - The new scribble object. * @param scribble - The new scribble object.
* @public * @public
*/ */
setScribble(scribble: TLScribble | null = null) { setScribble(scribble: TLScribble | null = null): this {
this.updateInstanceState({ scribble }, true) this.updateInstanceState({ scribble }, true)
return this return this
} }
@ -7254,7 +7354,7 @@ export class App extends EventEmitter {
* @param brush - The brush box model to set, or null for no brush model. * @param brush - The brush box model to set, or null for no brush model.
* @public * @public
*/ */
setBrush(brush: Box2dModel | null = null) { setBrush(brush: Box2dModel | null = null): this {
if (!brush && !this.brush) return this if (!brush && !this.brush) return this
this.updateInstanceState({ brush }, true) this.updateInstanceState({ brush }, true)
return this return this
@ -7273,7 +7373,7 @@ export class App extends EventEmitter {
* @param zoomBrush - The zoom box model to set, or null for no zoom model. * @param zoomBrush - The zoom box model to set, or null for no zoom model.
* @public * @public
*/ */
setZoomBrush(zoomBrush: Box2dModel | null = null) { setZoomBrush(zoomBrush: Box2dModel | null = null): this {
if (!zoomBrush && !this.zoomBrush) return this if (!zoomBrush && !this.zoomBrush) return this
this.updateInstanceState({ zoomBrush }, true) this.updateInstanceState({ zoomBrush }, true)
return this return this
@ -7297,6 +7397,7 @@ export class App extends EventEmitter {
const snapshot = getRotationSnapshot({ app: this }) const snapshot = getRotationSnapshot({ app: this })
applyRotationToSnapshotShapes({ delta, snapshot, app: this, stage: 'one-off' }) applyRotationToSnapshotShapes({ delta, snapshot, app: this, stage: 'one-off' })
return this return this
} }
@ -7703,7 +7804,10 @@ export class App extends EventEmitter {
private _setCamera(x: number, y: number, z = this.camera.z) { private _setCamera(x: number, y: number, z = this.camera.z) {
const currentCamera = this.camera const currentCamera = this.camera
if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) return this if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) return this
this.store.put([{ ...currentCamera, x, y, z }]) const nextCamera = { ...currentCamera, x, y, z }
this.batch(() => {
this.store.put([nextCamera])
const { currentScreenPoint } = this.inputs const { currentScreenPoint } = this.inputs
@ -7725,8 +7829,7 @@ export class App extends EventEmitter {
}) })
this._cameraManager.tick() this._cameraManager.tick()
})
this.emit('change-camera', this.camera)
return this return this
} }
@ -7829,6 +7932,28 @@ export class App extends EventEmitter {
return this return this
} }
/**
* Move the camera to the nearest content.
*
* @public
*/
zoomToContent() {
const bounds = this.selectedPageBounds ?? this.allShapesCommonBounds
if (bounds) {
this.zoomToBounds(
bounds.minX,
bounds.minY,
bounds.width,
bounds.height,
Math.min(1, this.zoomLevel),
{ duration: 220 }
)
}
return this
}
/** /**
* Zoom the camera to fit the current page's content in the viewport. * Zoom the camera to fit the current page's content in the viewport.
* *
@ -7880,6 +8005,7 @@ export class App extends EventEmitter {
} else { } else {
this.setCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1) this.setCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1)
} }
return this return this
} }
@ -7923,6 +8049,7 @@ export class App extends EventEmitter {
} else { } else {
this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom) this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom)
} }
return this return this
} }
@ -7967,6 +8094,7 @@ export class App extends EventEmitter {
} else { } else {
this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom) this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom)
} }
return this return this
} }
@ -7998,6 +8126,7 @@ export class App extends EventEmitter {
Math.max(1, this.camera.z), Math.max(1, this.camera.z),
opts opts
) )
return this return this
} }
@ -8711,6 +8840,60 @@ export class App extends EventEmitter {
return this return this
} }
// checkTracking(
// type: 'change' | 'create' | 'delete',
// prev: TLRecord | null,
// next: TLRecord | null
// ) {
// if (type === 'create' && next) {
// if (next && next.typeName === 'page') {
// this.trackEvent('page.add')
// }
// } else if (type === 'delete' && prev) {
// if (prev.typeName === 'page') {
// this.trackEvent('page.remove')
// }
// } else if (prev && next && type === 'change') {
// if (prev.typeName === 'page' && next.typeName === 'page' && prev.name !== next.name) {
// this.trackEvent('page.rename')
// }
// if (prev.typeName === 'instance' && next.typeName === 'instance') {
// // TODO: Not very performant
// for (const key of Object.keys(next.propsForNextShape)) {
// const prevValue = prev.propsForNextShape[key as keyof TLInstancePropsForNextShape]
// const nextValue = next.propsForNextShape[key as keyof TLInstancePropsForNextShape]
// if (prevValue !== nextValue) {
// this.trackEvent(`instance.propsForNextShape.${key}.change`, nextValue)
// }
// }
// if (prev.isToolLocked !== next.isToolLocked) {
// this.trackEvent('instance.isToolLocked.enabled', next.isToolLocked)
// }
// if (prev.isDebugMode !== next.isDebugMode) {
// this.trackEvent('instance.isDebugMode.enabled', next.isDebugMode)
// }
// if (prev.isFocusMode !== next.isFocusMode) {
// this.trackEvent('instance.isFocusMode.enabled', next.isFocusMode)
// }
// if (prev.currentPageId !== next.currentPageId) {
// this.trackEvent('instance.currentPageId.change')
// }
// }
// if (prev.typeName === 'user_document' && next.typeName === 'user_document') {
// if (prev.isDarkMode !== next.isDarkMode) {
// this.trackEvent('instance.isDarkMode.change', next.isDarkMode)
// }
// if (prev.isGridMode !== next.isGridMode) {
// this.trackEvent('instance.isGridMode.change', next.isGridMode)
// }
// if (prev.isSnapMode !== next.isSnapMode) {
// this.trackEvent('instance.isSnapMode.change', next.isSnapMode)
// }
// }
// }
// }
} }
function alertMaxShapes(app: App, pageId = app.currentPageId) { function alertMaxShapes(app: App, pageId = app.currentPageId) {

Wyświetl plik

@ -18,7 +18,11 @@ type CommandFn<Data> = (...args: any[]) =>
type ExtractData<Fn> = Fn extends CommandFn<infer Data> ? Data : never type ExtractData<Fn> = Fn extends CommandFn<infer Data> ? Data : never
type ExtractArgs<Fn> = Parameters<Extract<Fn, (...args: any[]) => any>> type ExtractArgs<Fn> = Parameters<Extract<Fn, (...args: any[]) => any>>
export class HistoryManager<CTX extends { emit: (name: string) => void }> { export class HistoryManager<
CTX extends {
emit: (name: 'change-history' | 'mark-history', ...args: any) => void
}
> {
_undos = atom<Stack<TLHistoryEntry>>('HistoryManager.undos', stack()) // Updated by each action that includes and undo _undos = atom<Stack<TLHistoryEntry>>('HistoryManager.undos', stack()) // Updated by each action that includes and undo
_redos = atom<Stack<TLHistoryEntry>>('HistoryManager.redos', stack()) // Updated when a user undoes _redos = atom<Stack<TLHistoryEntry>>('HistoryManager.redos', stack()) // Updated when a user undoes
_batchDepth = 0 // A flag for whether the user is in a batch operation _batchDepth = 0 // A flag for whether the user is in a batch operation
@ -103,7 +107,7 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
this._redos.set(stack()) this._redos.set(stack())
} }
this.ctx.emit('change-history') this.ctx.emit('change-history', { reason: 'push' })
} }
return this.ctx return this.ctx
@ -165,7 +169,6 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
}) => { }) => {
this.ignoringUpdates((undos, redos) => { this.ignoringUpdates((undos, redos) => {
if (undos.length === 0) { if (undos.length === 0) {
this.ctx.emit('change-history')
return { undos, redos } return { undos, redos }
} }
@ -176,13 +179,19 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
redos = redos.push(mark) redos = redos.push(mark)
} }
if (mark.id === toMark) { if (mark.id === toMark) {
this.ctx.emit('change-history') this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos } return { undos, redos }
} }
} }
if (undos.length === 0) { if (undos.length === 0) {
this.ctx.emit('change-history') this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos } return { undos, redos }
} }
@ -196,7 +205,10 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
if (command.type === 'STOP') { if (command.type === 'STOP') {
if (command.onUndo && (!toMark || command.id === toMark)) { if (command.onUndo && (!toMark || command.id === toMark)) {
this.ctx.emit('change-history') this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos } return { undos, redos }
} }
} else { } else {
@ -205,7 +217,10 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
} }
} }
this.ctx.emit('change-history') this.ctx.emit(
'change-history',
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
)
return { undos, redos } return { undos, redos }
}) })
@ -221,7 +236,6 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
redo = () => { redo = () => {
this.ignoringUpdates((undos, redos) => { this.ignoringUpdates((undos, redos) => {
if (redos.length === 0) { if (redos.length === 0) {
this.ctx.emit('change-history')
return { undos, redos } return { undos, redos }
} }
@ -231,7 +245,7 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
} }
if (redos.length === 0) { if (redos.length === 0) {
this.ctx.emit('change-history') this.ctx.emit('change-history', { reason: 'redo' })
return { undos, redos } return { undos, redos }
} }
@ -254,7 +268,7 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
} }
} }
this.ctx.emit('change-history') this.ctx.emit('change-history', { reason: 'redo' })
return { undos, redos } return { undos, redos }
}) })
@ -284,6 +298,8 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
this._undos.update((undos) => undos.push({ type: 'STOP', id, onUndo, onRedo })) this._undos.update((undos) => undos.push({ type: 'STOP', id, onUndo, onRedo }))
this.ctx.emit('mark-history', { id })
return id return id
} }

Wyświetl plik

@ -0,0 +1,22 @@
import { TLPageId, TLRecord } from '@tldraw/tlschema'
import { TLChange } from '../App'
import { TLEventInfo } from './event-types'
/** @public */
export interface TLEventMap {
// Lifecycle / Internal
mount: []
'max-shapes': [{ name: string; pageId: TLPageId; count: number }]
change: [TLChange<TLRecord>]
update: []
crash: [{ error: unknown }]
'stop-camera-animation': []
'stop-following': []
event: [TLEventInfo]
tick: [number]
'change-history': [{ reason: 'undo' | 'redo' | 'push' } | { reason: 'bail'; markId?: string }]
'mark-history': [{ id: string }]
}
/** @public */
export type TLEventMapHandler<T extends keyof TLEventMap> = (...args: TLEventMap[T]) => void

Wyświetl plik

@ -1,2 +0,0 @@
/** @public */
export type TLReorderOperation = 'toBack' | 'toFront' | 'forward' | 'backward'

Wyświetl plik

@ -45,9 +45,9 @@ describe('distributeShapes command', () => {
describe('when less than three shapes are selected', () => { describe('when less than three shapes are selected', () => {
it('does nothing', () => { it('does nothing', () => {
const fn = jest.fn()
app.on('update_node', fn)
app.setSelectedIds([ids.boxA, ids.boxB]) app.setSelectedIds([ids.boxA, ids.boxB])
const fn = jest.fn()
app.on('change-history', fn)
app.distributeShapes('horizontal') app.distributeShapes('horizontal')
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
expect(fn).not.toHaveBeenCalled() expect(fn).not.toHaveBeenCalled()

Wyświetl plik

@ -50,9 +50,9 @@ describe('distributeShapes command', () => {
describe('when less than three shapes are selected', () => { describe('when less than three shapes are selected', () => {
it('does nothing', () => { it('does nothing', () => {
const fn = jest.fn()
app.on('update_node', fn)
app.setSelectedIds([ids.boxA, ids.boxB]) app.setSelectedIds([ids.boxA, ids.boxB])
const fn = jest.fn()
app.on('change-history', fn)
app.stackShapes('horizontal') app.stackShapes('horizontal')
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
expect(fn).not.toHaveBeenCalled() expect(fn).not.toHaveBeenCalled()

Wyświetl plik

@ -69,9 +69,9 @@ beforeEach(() => {
describe('when less than two shapes are selected', () => { describe('when less than two shapes are selected', () => {
it('does nothing', () => { it('does nothing', () => {
const fn = jest.fn()
app.on('update_shape', fn)
app.setSelectedIds([ids.boxB]) app.setSelectedIds([ids.boxB])
const fn = jest.fn()
app.on('change-history', fn)
app.stretchShapes('horizontal') app.stretchShapes('horizontal')
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)

Wyświetl plik

@ -111,7 +111,7 @@ describe('When dragging the arrow', () => {
}) })
it('returns to arrow.idle, keeping shape, on pointer up when tool lock is active', () => { it('returns to arrow.idle, keeping shape, on pointer up when tool lock is active', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
const shapesBefore = app.shapesArray.length const shapesBefore = app.shapesArray.length
app app
.setSelectedTool('arrow') .setSelectedTool('arrow')

Wyświetl plik

@ -123,7 +123,7 @@ describe('When in the pointing state', () => {
}) })
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => { it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
expect(app.shapesArray.length).toBe(0) expect(app.shapesArray.length).toBe(0)
app.setSelectedTool('frame') app.setSelectedTool('frame')
app.pointerDown(50, 50) app.pointerDown(50, 50)
@ -152,7 +152,7 @@ describe('When in the resizing state', () => {
}) })
it('Returns to frame.idle on complete if tool lock is enabled', () => { it('Returns to frame.idle on complete if tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app.setSelectedTool('frame') app.setSelectedTool('frame')
app.pointerDown(50, 50) app.pointerDown(50, 50)
app.pointerMove(100, 100) app.pointerMove(100, 100)

Wyświetl plik

@ -152,7 +152,7 @@ describe('When in the pointing state', () => {
}) })
it('Creates a geo and returns to geo.idle on pointer up if tool lock is enabled', () => { it('Creates a geo and returns to geo.idle on pointer up if tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
expect(app.shapesArray.length).toBe(0) expect(app.shapesArray.length).toBe(0)
app.setSelectedTool('geo') app.setSelectedTool('geo')
app.pointerDown(50, 50) app.pointerDown(50, 50)
@ -181,7 +181,7 @@ describe('When in the resizing state while creating a geo shape', () => {
}) })
it('Returns to geo.idle on complete if tool lock is enabled', () => { it('Returns to geo.idle on complete if tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app.setSelectedTool('geo') app.setSelectedTool('geo')
app.pointerDown(50, 50) app.pointerDown(50, 50)
app.pointerMove(100, 100) app.pointerMove(100, 100)

Wyświetl plik

@ -89,7 +89,7 @@ describe('When dragging the line', () => {
}) })
it('returns to line.idle, keeping shape, on pointer up if tool lock is enabled', () => { it('returns to line.idle, keeping shape, on pointer up if tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
const shapesBefore = app.shapesArray.length const shapesBefore = app.shapesArray.length
app app
.setSelectedTool('line') .setSelectedTool('line')
@ -113,7 +113,7 @@ describe('When dragging the line', () => {
describe('When extending the line with the shift-key in tool-lock mode', () => { describe('When extending the line with the shift-key in tool-lock mode', () => {
it('extends a line by joining-the-dots', () => { it('extends a line by joining-the-dots', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app app
.setSelectedTool('line') .setSelectedTool('line')
.pointerDown(0, 0, { target: 'canvas' }) .pointerDown(0, 0, { target: 'canvas' })
@ -130,7 +130,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
}) })
it('extends a line after a click by shift-click dragging', () => { it('extends a line after a click by shift-click dragging', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app app
.setSelectedTool('line') .setSelectedTool('line')
.pointerDown(0, 0, { target: 'canvas' }) .pointerDown(0, 0, { target: 'canvas' })
@ -147,7 +147,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
}) })
it('extends a line by shift-click dragging', () => { it('extends a line by shift-click dragging', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app app
.setSelectedTool('line') .setSelectedTool('line')
.pointerDown(0, 0, { target: 'canvas' }) .pointerDown(0, 0, { target: 'canvas' })
@ -165,7 +165,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
}) })
it('extends a line by shift-clicking even after canceling a pointerdown', () => { it('extends a line by shift-clicking even after canceling a pointerdown', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app app
.setSelectedTool('line') .setSelectedTool('line')
.pointerDown(0, 0, { target: 'canvas' }) .pointerDown(0, 0, { target: 'canvas' })
@ -185,7 +185,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
}) })
it('extends a line by shift-clicking even after canceling a pointermove', () => { it('extends a line by shift-clicking even after canceling a pointermove', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app app
.setSelectedTool('line') .setSelectedTool('line')
.pointerDown(0, 0, { target: 'canvas' }) .pointerDown(0, 0, { target: 'canvas' })

Wyświetl plik

@ -110,7 +110,7 @@ describe('When in the pointing state', () => {
}) })
it('Returns to the note tool on complete from translating when tool lock is enabled', () => { it('Returns to the note tool on complete from translating when tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
app.setSelectedTool('note') app.setSelectedTool('note')
app.pointerDown(50, 50) app.pointerDown(50, 50)
app.pointerMove(55, 55) app.pointerMove(55, 55)
@ -135,7 +135,7 @@ describe('When in the pointing state', () => {
}) })
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => { it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
app.updateInstanceState({ isToolLocked: true }) app.setToolLocked(true)
expect(app.shapesArray.length).toBe(0) expect(app.shapesArray.length).toBe(0)
app.setSelectedTool('note') app.setSelectedTool('note')
app.pointerDown(50, 50) app.pointerDown(50, 50)

Wyświetl plik

@ -39,7 +39,7 @@ export interface ActionItem {
// (undocumented) // (undocumented)
menuLabel?: TLTranslationKey; menuLabel?: TLTranslationKey;
// (undocumented) // (undocumented)
onSelect: () => Promise<void> | void; onSelect: (source: TLUiEventSource) => Promise<void> | void;
// (undocumented) // (undocumented)
readonlyOk: boolean; readonlyOk: boolean;
// (undocumented) // (undocumented)
@ -636,7 +636,7 @@ export const TldrawUi: React_2.NamedExoticComponent<{
export const TldrawUiContent: React_2.NamedExoticComponent<TldrawUiContentProps>; export const TldrawUiContent: React_2.NamedExoticComponent<TldrawUiContentProps>;
// @public (undocumented) // @public (undocumented)
export function TldrawUiContextProvider({ overrides, assetUrls, children, }: TldrawUiContextProviderProps): JSX.Element; export function TldrawUiContextProvider({ overrides, assetUrls, onEvent, children, }: TldrawUiContextProviderProps): JSX.Element;
// @public (undocumented) // @public (undocumented)
export interface TldrawUiContextProviderProps { export interface TldrawUiContextProviderProps {
@ -645,6 +645,8 @@ export interface TldrawUiContextProviderProps {
// (undocumented) // (undocumented)
children?: any; children?: any;
// (undocumented) // (undocumented)
onEvent?: TLUiEventHandler;
// (undocumented)
overrides?: TldrawUiOverrides | TldrawUiOverrides[]; overrides?: TldrawUiOverrides | TldrawUiOverrides[];
} }

Wyświetl plik

@ -113,7 +113,7 @@ export const TldrawUiContent = React.memo(function TldrawUI({
className="tlui-focus-button" className="tlui-focus-button"
title={`${msg('focus-mode.toggle-focus-mode')}`} title={`${msg('focus-mode.toggle-focus-mode')}`}
icon="dot" icon="dot"
onClick={toggleFocus.onSelect} onClick={() => toggleFocus.onSelect('menu')}
/> />
</div> </div>
) : ( ) : (

Wyświetl plik

@ -5,6 +5,7 @@ import { AssetUrlsProvider } from './hooks/useAssetUrls'
import { BreakPointProvider } from './hooks/useBreakpoint' import { BreakPointProvider } from './hooks/useBreakpoint'
import { ContextMenuSchemaProvider } from './hooks/useContextMenuSchema' import { ContextMenuSchemaProvider } from './hooks/useContextMenuSchema'
import { DialogsProvider } from './hooks/useDialogsProvider' import { DialogsProvider } from './hooks/useDialogsProvider'
import { EventsProvider, TLUiEventHandler } from './hooks/useEventsProvider'
import { HelpMenuSchemaProvider } from './hooks/useHelpMenuSchema' import { HelpMenuSchemaProvider } from './hooks/useHelpMenuSchema'
import { KeyboardShortcutsSchemaProvider } from './hooks/useKeyboardShortcutsSchema' import { KeyboardShortcutsSchemaProvider } from './hooks/useKeyboardShortcutsSchema'
import { MenuSchemaProvider } from './hooks/useMenuSchema' import { MenuSchemaProvider } from './hooks/useMenuSchema'
@ -18,6 +19,7 @@ import { TldrawUiOverrides, useMergedOverrides, useMergedTranslationOverrides }
export interface TldrawUiContextProviderProps { export interface TldrawUiContextProviderProps {
assetUrls?: UiAssetUrls assetUrls?: UiAssetUrls
overrides?: TldrawUiOverrides | TldrawUiOverrides[] overrides?: TldrawUiOverrides | TldrawUiOverrides[]
onEvent?: TLUiEventHandler
children?: any children?: any
} }
@ -25,6 +27,7 @@ export interface TldrawUiContextProviderProps {
export function TldrawUiContextProvider({ export function TldrawUiContextProvider({
overrides, overrides,
assetUrls, assetUrls,
onEvent,
children, children,
}: TldrawUiContextProviderProps) { }: TldrawUiContextProviderProps) {
return ( return (
@ -33,7 +36,9 @@ export function TldrawUiContextProvider({
<ToastsProvider> <ToastsProvider>
<DialogsProvider> <DialogsProvider>
<BreakPointProvider> <BreakPointProvider>
<EventsProvider onEvent={onEvent}>
<InternalProviders overrides={overrides}>{children}</InternalProviders> <InternalProviders overrides={overrides}>{children}</InternalProviders>
</EventsProvider>
</BreakPointProvider> </BreakPointProvider>
</DialogsProvider> </DialogsProvider>
</ToastsProvider> </ToastsProvider>

Wyświetl plik

@ -37,7 +37,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
? `${kbdStr(kbd)}` ? `${kbdStr(kbd)}`
: '' : ''
} }
onClick={onSelect} onClick={() => onSelect('actions-menu')}
disabled={item.disabled} disabled={item.disabled}
/> />
) )

Wyświetl plik

@ -42,7 +42,7 @@ export function BackToContent() {
iconLeft={action.icon} iconLeft={action.icon}
label={action.label} label={action.label}
onClick={() => { onClick={() => {
action.onSelect() action.onSelect('helper-buttons')
setShowBackToContent(false) setShowBackToContent(false)
}} }}
/> />

Wyświetl plik

@ -164,7 +164,7 @@ function ContextMenuContent() {
dir="ltr" dir="ltr"
disabled={item.disabled} disabled={item.disabled}
onSelect={(e) => { onSelect={(e) => {
onSelect() onSelect('context-menu')
preventDefault(e) preventDefault(e)
}} }}
title={labelStr ? labelStr : undefined} title={labelStr ? labelStr : undefined}
@ -199,7 +199,7 @@ function ContextMenuContent() {
if (disableClicks) { if (disableClicks) {
setDisableClicks(false) setDisableClicks(false)
} else { } else {
onSelect() onSelect('context-menu')
} }
}} }}
/> />

Wyświetl plik

@ -16,7 +16,7 @@ export const DuplicateButton = track(function DuplicateButton() {
return ( return (
<Button <Button
icon={action.icon} icon={action.icon}
onClick={action.onSelect} onClick={() => action.onSelect('quick-actions')}
disabled={noSelected} disabled={noSelected}
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`} title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
smallIcon smallIcon

Wyświetl plik

@ -25,7 +25,7 @@ export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps)
const selectedShape = app.onlySelectedShape const selectedShape = app.onlySelectedShape
const [validState, setIsValid] = useState(valiateUrl(selectedShape?.props.url)) const [validState, setValid] = useState(valiateUrl(selectedShape?.props.url))
const rInitialValue = useRef(selectedShape?.props.url) const rInitialValue = useRef(selectedShape?.props.url)
@ -46,7 +46,7 @@ export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps)
setUrlValue(value) setUrlValue(value)
const validStateUrl = valiateUrl(value.trim()) const validStateUrl = valiateUrl(value.trim())
setIsValid((s) => (s === validStateUrl ? s : validStateUrl)) setValid((s) => (s === validStateUrl ? s : validStateUrl))
if (validStateUrl) { if (validStateUrl) {
rValue.current = value rValue.current = value
} }

Wyświetl plik

@ -88,7 +88,15 @@ function HelpMenuContent() {
} }
case 'item': { case 'item': {
const { id, kbd, label, onSelect, icon } = item.actionItem const { id, kbd, label, onSelect, icon } = item.actionItem
return <M.Item key={id} kbd={kbd} label={label} onClick={onSelect} iconLeft={icon} /> return (
<M.Item
key={id}
kbd={kbd}
label={label}
onClick={() => onSelect('help-menu')}
iconLeft={icon}
/>
)
} }
} }
} }

Wyświetl plik

@ -121,7 +121,7 @@ function MenuContent() {
return ( return (
<M.CheckboxItem <M.CheckboxItem
key={id} key={id}
onSelect={onSelect} onSelect={() => onSelect('menu')}
title={labelStr ? labelStr : ''} title={labelStr ? labelStr : ''}
checked={item.checked} checked={item.checked}
disabled={item.disabled} disabled={item.disabled}
@ -139,7 +139,7 @@ function MenuContent() {
data-wd={`menu-item.${item.id}`} data-wd={`menu-item.${item.id}`}
kbd={kbd} kbd={kbd}
label={labelToUse} label={labelToUse}
onClick={onSelect} onClick={() => onSelect('menu')}
disabled={item.disabled} disabled={item.disabled}
/> />
) )

Wyświetl plik

@ -1,6 +1,7 @@
import * as _ContextMenu from '@radix-ui/react-context-menu' import * as _ContextMenu from '@radix-ui/react-context-menu'
import { TLPage, TLPageId, useApp, useContainer } from '@tldraw/editor' import { TLPage, TLPageId, useApp, useContainer } from '@tldraw/editor'
import { track } from 'signia-react' import { track } from 'signia-react'
import { useToasts } from '../hooks/useToastsProvider'
import { useTranslation } from '../hooks/useTranslation/useTranslation' import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button' import { Button } from './primitives/Button'
@ -10,6 +11,7 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
const pages = app.pages const pages = app.pages
const currentPageId = app.currentPageId const currentPageId = app.currentPageId
const msg = useTranslation() const msg = useTranslation()
const { addToast } = useToasts()
return ( return (
<_ContextMenu.Sub> <_ContextMenu.Sub>
@ -36,6 +38,25 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
onSelect={() => { onSelect={() => {
app.mark('move_shapes_to_page') app.mark('move_shapes_to_page')
app.moveShapesToPage(app.selectedIds, page.id as TLPageId) app.moveShapesToPage(app.selectedIds, page.id as TLPageId)
const toPage = app.getPageById(page.id)
if (toPage) {
addToast({
title: 'Changed Page',
description: `Moved to ${toPage.name}.`,
actions: [
{
label: 'Go Back',
type: 'primary',
onClick: () => {
app.mark('change-page')
app.setCurrentPageId(currentPageId)
},
},
],
})
}
}} }}
asChild asChild
> >

Wyświetl plik

@ -46,14 +46,14 @@ export const NavigationZone = memo(function NavigationZone() {
icon="minus" icon="minus"
data-wd="minimap.zoom-out" data-wd="minimap.zoom-out"
title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`} title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`}
onClick={actions['zoom-out'].onSelect} onClick={() => actions['zoom-out'].onSelect('navigation-zone')}
/> />
<ZoomMenu /> <ZoomMenu />
<Button <Button
icon="plus" icon="plus"
data-wd="minimap.zoom-in" data-wd="minimap.zoom-in"
title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`} title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`}
onClick={actions['zoom-in'].onSelect} onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
/> />
<Button <Button
title={msg('navigation-zone.toggle-minimap')} title={msg('navigation-zone.toggle-minimap')}

Wyświetl plik

@ -78,7 +78,7 @@ function ZoomMenuItem(props: {
label={actions[action].label} label={actions[action].label}
kbd={actions[action].kbd} kbd={actions[action].kbd}
data-wd={props['data-wd']} data-wd={props['data-wd']}
onClick={actions[action].onSelect} onClick={() => actions[action].onSelect('zoom-menu')}
noClose={noClose} noClose={noClose}
disabled={disabled} disabled={disabled}
/> />

Wyświetl plik

@ -14,5 +14,11 @@ export const ExitPenMode = track(function ExitPenMode() {
const action = actions['exit-pen-mode'] const action = actions['exit-pen-mode']
return <Button label={action.label} iconLeft={action.icon} onClick={action.onSelect} /> return (
<Button
label={action.label}
iconLeft={action.icon}
onClick={() => action.onSelect('helper-buttons')}
/>
)
}) })

Wyświetl plik

@ -18,7 +18,7 @@ export const RedoButton = memo(function RedoButton() {
icon={redo.icon} icon={redo.icon}
title={`${msg(redo.label!)} ${kbdStr(redo.kbd!)}`} title={`${msg(redo.label!)} ${kbdStr(redo.kbd!)}`}
disabled={!canRedo} disabled={!canRedo}
onClick={redo.onSelect} onClick={() => redo.onSelect('quick-actions')}
smallIcon smallIcon
/> />
) )

Wyświetl plik

@ -13,5 +13,11 @@ export const StopFollowing = track(function ExitPenMode() {
const action = actions['stop-following'] const action = actions['stop-following']
return <Button label={action.label} iconLeft={action.icon} onClick={action.onSelect} /> return (
<Button
label={action.label}
iconLeft={action.icon}
onClick={() => action.onSelect('people-menu')}
/>
)
}) })

Wyświetl plik

@ -214,6 +214,7 @@ const OverflowToolsContent = track(function OverflowToolsContent({
toolbarItems: ToolbarItem[] toolbarItems: ToolbarItem[]
}) { }) {
const msg = useTranslation() const msg = useTranslation()
return ( return (
<div className="tlui-button-grid__four tlui-button-grid__reverse"> <div className="tlui-button-grid__four tlui-button-grid__reverse">
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => { {toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => {

Wyświetl plik

@ -13,6 +13,7 @@ export const TrashButton = track(function TrashButton() {
const action = actions['delete'] const action = actions['delete']
const isReadonly = useReadonly() const isReadonly = useReadonly()
if (isReadonly) return null if (isReadonly) return null
const noSelected = app.selectedIds.length <= 0 const noSelected = app.selectedIds.length <= 0
@ -20,7 +21,7 @@ export const TrashButton = track(function TrashButton() {
return ( return (
<Button <Button
icon={action.icon} icon={action.icon}
onClick={action.onSelect} onClick={() => action.onSelect('quick-actions')}
disabled={noSelected} disabled={noSelected}
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`} title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
smallIcon smallIcon

Wyświetl plik

@ -18,7 +18,7 @@ export const UndoButton = memo(function UndoButton() {
icon={undo.icon} icon={undo.icon}
title={`${msg(undo.label!)} ${kbdStr(undo.kbd!)}`} title={`${msg(undo.label!)} ${kbdStr(undo.kbd!)}`}
disabled={!canUndo} disabled={!canUndo}
onClick={undo.onSelect} onClick={() => undo.onSelect('quick-actions')}
smallIcon smallIcon
/> />
) )

Wyświetl plik

@ -21,6 +21,7 @@ import { TLUiIconType } from '../icon-types'
import { useMenuClipboardEvents } from './useClipboardEvents' import { useMenuClipboardEvents } from './useClipboardEvents'
import { useCopyAs } from './useCopyAs' import { useCopyAs } from './useCopyAs'
import { useDialogs } from './useDialogsProvider' import { useDialogs } from './useDialogsProvider'
import { TLUiEventSource, useEvents } from './useEventsProvider'
import { useExportAs } from './useExportAs' import { useExportAs } from './useExportAs'
import { useInsertMedia } from './useInsertMedia' import { useInsertMedia } from './useInsertMedia'
import { usePrint } from './usePrint' import { usePrint } from './usePrint'
@ -39,7 +40,7 @@ export interface ActionItem {
contextMenuLabel?: TLTranslationKey contextMenuLabel?: TLTranslationKey
readonlyOk: boolean readonlyOk: boolean
checkbox?: boolean checkbox?: boolean
onSelect: () => Promise<void> | void onSelect: (source: TLUiEventSource) => Promise<void> | void
} }
/** @public */ /** @public */
@ -76,46 +77,18 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
const copyAs = useCopyAs() const copyAs = useCopyAs()
const exportAs = useExportAs() const exportAs = useExportAs()
const trackEvent = useEvents()
// should this be a useMemo? looks like it doesn't actually deref any reactive values // should this be a useMemo? looks like it doesn't actually deref any reactive values
const actions = React.useMemo<ActionsContextType>(() => { const actions = React.useMemo<ActionsContextType>(() => {
const actions = makeActions([ const actions = makeActions([
// 'new-project': {
// id: 'file.new',
// label: 'file.new',
// onSelect() {
// newFile()
// },
// },
// 'open-project': {
// id: 'file.open',
// label: 'file.open',
// kbd: '$o',
// onSelect() {
// openFile()
// },
// },
// 'save-project': {
// id: 'file.save',
// label: 'file.save',
// kbd: '$s',
// onSelect() {
// saveFile()
// },
// },
// 'save-project-as': {
// id: 'file.save-as',
// label: 'file.save-as',
// kbd: '$!s',
// onSelect() {
// saveFileAs()
// },
// },
{ {
id: 'edit-link', id: 'edit-link',
label: 'action.edit-link', label: 'action.edit-link',
icon: 'link', icon: 'link',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'edit-link')
app.mark('edit-link') app.mark('edit-link')
addDialog({ component: EditLinkDialog }) addDialog({ component: EditLinkDialog })
}, },
@ -125,7 +98,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.insert-embed', label: 'action.insert-embed',
readonlyOk: false, readonlyOk: false,
kbd: '$i', kbd: '$i',
onSelect() { onSelect(source) {
trackEvent(source, 'insert-embed')
addDialog({ component: EmbedDialog }) addDialog({ component: EmbedDialog })
}, },
}, },
@ -134,7 +108,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.insert-media', label: 'action.insert-media',
kbd: '$u', kbd: '$u',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'insert-media')
insertMedia() insertMedia()
}, },
}, },
@ -144,7 +119,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'undo', icon: 'undo',
kbd: '$z', kbd: '$z',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'undo')
app.undo() app.undo()
}, },
}, },
@ -154,7 +130,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'redo', icon: 'redo',
kbd: '$!z', kbd: '$!z',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'redo')
app.redo() app.redo()
}, },
}, },
@ -164,7 +141,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.export-as-svg.short', menuLabel: 'action.export-as-svg.short',
contextMenuLabel: 'action.export-as-svg.short', contextMenuLabel: 'action.export-as-svg.short',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'export-as', { format: 'svg' })
exportAs(app.selectedIds, 'svg') exportAs(app.selectedIds, 'svg')
}, },
}, },
@ -174,7 +152,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.export-as-png.short', menuLabel: 'action.export-as-png.short',
contextMenuLabel: 'action.export-as-png.short', contextMenuLabel: 'action.export-as-png.short',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'export-as', { format: 'png' })
exportAs(app.selectedIds, 'png') exportAs(app.selectedIds, 'png')
}, },
}, },
@ -184,7 +163,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.export-as-json.short', menuLabel: 'action.export-as-json.short',
contextMenuLabel: 'action.export-as-json.short', contextMenuLabel: 'action.export-as-json.short',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'export-as', { format: 'json' })
exportAs(app.selectedIds, 'json') exportAs(app.selectedIds, 'json')
}, },
}, },
@ -195,7 +175,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.copy-as-svg.short', contextMenuLabel: 'action.copy-as-svg.short',
kbd: '$!c', kbd: '$!c',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'copy-as', { format: 'svg' })
copyAs(app.selectedIds, 'svg') copyAs(app.selectedIds, 'svg')
}, },
}, },
@ -205,7 +186,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.copy-as-png.short', menuLabel: 'action.copy-as-png.short',
contextMenuLabel: 'action.copy-as-png.short', contextMenuLabel: 'action.copy-as-png.short',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'copy-as', { format: 'png' })
copyAs(app.selectedIds, 'png') copyAs(app.selectedIds, 'png')
}, },
}, },
@ -215,7 +197,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.copy-as-json.short', menuLabel: 'action.copy-as-json.short',
contextMenuLabel: 'action.copy-as-json.short', contextMenuLabel: 'action.copy-as-json.short',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'copy-as', { format: 'json' })
copyAs(app.selectedIds, 'json') copyAs(app.selectedIds, 'json')
}, },
}, },
@ -223,7 +206,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
id: 'toggle-auto-size', id: 'toggle-auto-size',
label: 'action.toggle-auto-size', label: 'action.toggle-auto-size',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'toggle-auto-size')
app.mark() app.mark()
app.updateShapes( app.updateShapes(
app.selectedShapes app.selectedShapes
@ -246,7 +230,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
id: 'open-embed-link', id: 'open-embed-link',
label: 'action.open-embed-link', label: 'action.open-embed-link',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'open-embed-link')
const ids = app.selectedIds const ids = app.selectedIds
const warnMsg = 'No embed shapes selected' const warnMsg = 'No embed shapes selected'
if (ids.length !== 1) { if (ids.length !== 1) {
@ -266,7 +251,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
id: 'convert-to-bookmark', id: 'convert-to-bookmark',
label: 'action.convert-to-bookmark', label: 'action.convert-to-bookmark',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'convert-to-bookmark')
const ids = app.selectedIds const ids = app.selectedIds
const shapes = ids.map((id) => app.getShapeById(id)) const shapes = ids.map((id) => app.getShapeById(id))
@ -308,7 +294,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
id: 'convert-to-embed', id: 'convert-to-embed',
label: 'action.convert-to-embed', label: 'action.convert-to-embed',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'convert-to-embed')
const ids = app.selectedIds const ids = app.selectedIds
const shapes = compact(ids.map((id) => app.getShapeById(id))) const shapes = compact(ids.map((id) => app.getShapeById(id)))
@ -358,8 +345,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.duplicate', label: 'action.duplicate',
icon: 'duplicate', icon: 'duplicate',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
if (app.currentToolId !== 'select') return if (app.currentToolId !== 'select') return
trackEvent(source, 'duplicate-shapes')
const ids = app.selectedIds const ids = app.selectedIds
const commonBounds = Box2d.Common(compact(ids.map((id) => app.getPageBoundsById(id)))) const commonBounds = Box2d.Common(compact(ids.map((id) => app.getPageBoundsById(id))))
const offset = app.canMoveCamera const offset = app.canMoveCamera
@ -381,7 +369,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '$!g', kbd: '$!g',
icon: 'ungroup', icon: 'ungroup',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'ungroup-shapes')
app.mark('ungroup') app.mark('ungroup')
app.ungroupShapes(app.selectedIds) app.ungroupShapes(app.selectedIds)
}, },
@ -392,7 +381,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '$g', kbd: '$g',
icon: 'group', icon: 'group',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'group-shapes')
if (app.selectedShapes.length === 1 && app.selectedShapes[0].type === 'group') { if (app.selectedShapes.length === 1 && app.selectedShapes[0].type === 'group') {
app.mark('ungroup') app.mark('ungroup')
app.ungroupShapes(app.selectedIds) app.ungroupShapes(app.selectedIds)
@ -408,7 +398,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?A', kbd: '?A',
icon: 'align-left', icon: 'align-left',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'align-shapes', { operation: 'left' })
app.mark('align left') app.mark('align left')
app.alignShapes('left', app.selectedIds) app.alignShapes('left', app.selectedIds)
}, },
@ -420,7 +411,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?H', kbd: '?H',
icon: 'align-center-horizontal', icon: 'align-center-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'align-shapes', { operation: 'center-horizontal' })
app.mark('align center horizontal') app.mark('align center horizontal')
app.alignShapes('center-horizontal', app.selectedIds) app.alignShapes('center-horizontal', app.selectedIds)
}, },
@ -431,7 +423,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?D', kbd: '?D',
icon: 'align-right', icon: 'align-right',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'align-shapes', { operation: 'right' })
app.mark('align right') app.mark('align right')
app.alignShapes('right', app.selectedIds) app.alignShapes('right', app.selectedIds)
}, },
@ -443,7 +436,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?V', kbd: '?V',
icon: 'align-center-vertical', icon: 'align-center-vertical',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'align-shapes', { operation: 'center-vertical' })
app.mark('align center vertical') app.mark('align center vertical')
app.alignShapes('center-vertical', app.selectedIds) app.alignShapes('center-vertical', app.selectedIds)
}, },
@ -454,7 +448,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'align-top', icon: 'align-top',
kbd: '?W', kbd: '?W',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'align-shapes', { operation: 'top' })
app.mark('align top') app.mark('align top')
app.alignShapes('top', app.selectedIds) app.alignShapes('top', app.selectedIds)
}, },
@ -465,7 +460,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'align-bottom', icon: 'align-bottom',
kbd: '?S', kbd: '?S',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'align-shapes', { operation: 'bottom' })
app.mark('align bottom') app.mark('align bottom')
app.alignShapes('bottom', app.selectedIds) app.alignShapes('bottom', app.selectedIds)
}, },
@ -476,7 +472,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.distribute-horizontal.short', contextMenuLabel: 'action.distribute-horizontal.short',
icon: 'distribute-horizontal', icon: 'distribute-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'distribute-shapes', { operation: 'horizontal' })
app.mark('distribute horizontal') app.mark('distribute horizontal')
app.distributeShapes('horizontal', app.selectedIds) app.distributeShapes('horizontal', app.selectedIds)
}, },
@ -487,7 +484,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.distribute-vertical.short', contextMenuLabel: 'action.distribute-vertical.short',
icon: 'distribute-vertical', icon: 'distribute-vertical',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'distribute-shapes', { operation: 'vertical' })
app.mark('distribute vertical') app.mark('distribute vertical')
app.distributeShapes('vertical', app.selectedIds) app.distributeShapes('vertical', app.selectedIds)
}, },
@ -498,7 +496,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.stretch-horizontal.short', contextMenuLabel: 'action.stretch-horizontal.short',
icon: 'stretch-horizontal', icon: 'stretch-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'stretch-shapes', { operation: 'horizontal' })
app.mark('stretch horizontal') app.mark('stretch horizontal')
app.stretchShapes('horizontal', app.selectedIds) app.stretchShapes('horizontal', app.selectedIds)
}, },
@ -509,7 +508,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.stretch-vertical.short', contextMenuLabel: 'action.stretch-vertical.short',
icon: 'stretch-vertical', icon: 'stretch-vertical',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'stretch-shapes', { operation: 'vertical' })
app.mark('stretch vertical') app.mark('stretch vertical')
app.stretchShapes('vertical', app.selectedIds) app.stretchShapes('vertical', app.selectedIds)
}, },
@ -520,7 +520,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.flip-horizontal.short', contextMenuLabel: 'action.flip-horizontal.short',
kbd: '!h', kbd: '!h',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'flip-shapes', { operation: 'horizontal' })
app.mark('flip horizontal') app.mark('flip horizontal')
app.flipShapes('horizontal', app.selectedIds) app.flipShapes('horizontal', app.selectedIds)
}, },
@ -531,7 +532,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.flip-vertical.short', contextMenuLabel: 'action.flip-vertical.short',
kbd: '!v', kbd: '!v',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'flip-shapes', { operation: 'vertical' })
app.mark('flip vertical') app.mark('flip vertical')
app.flipShapes('vertical', app.selectedIds) app.flipShapes('vertical', app.selectedIds)
}, },
@ -541,7 +543,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.pack', label: 'action.pack',
icon: 'pack', icon: 'pack',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'pack-shapes')
app.mark('pack') app.mark('pack')
app.packShapes(app.selectedIds) app.packShapes(app.selectedIds)
}, },
@ -552,7 +555,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.stack-vertical.short', contextMenuLabel: 'action.stack-vertical.short',
icon: 'stack-vertical', icon: 'stack-vertical',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'stack-shapes', { operation: 'vertical' })
app.mark('stack-vertical') app.mark('stack-vertical')
app.stackShapes('vertical', app.selectedIds) app.stackShapes('vertical', app.selectedIds)
}, },
@ -563,7 +567,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
contextMenuLabel: 'action.stack-horizontal.short', contextMenuLabel: 'action.stack-horizontal.short',
icon: 'stack-horizontal', icon: 'stack-horizontal',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'stack-shapes', { operation: 'horizontal' })
app.mark('stack-horizontal') app.mark('stack-horizontal')
app.stackShapes('horizontal', app.selectedIds) app.stackShapes('horizontal', app.selectedIds)
}, },
@ -574,7 +579,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: ']', kbd: ']',
icon: 'bring-to-front', icon: 'bring-to-front',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'reorder-shapes', { operation: 'toFront' })
app.mark('bring to front') app.mark('bring to front')
app.bringToFront() app.bringToFront()
}, },
@ -585,7 +591,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'bring-forward', icon: 'bring-forward',
kbd: '?]', kbd: '?]',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'reorder-shapes', { operation: 'forward' })
app.mark('bring forward') app.mark('bring forward')
app.bringForward() app.bringForward()
}, },
@ -596,7 +603,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'send-backward', icon: 'send-backward',
kbd: '?[', kbd: '?[',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'reorder-shapes', { operation: 'backward' })
app.mark('send backward') app.mark('send backward')
app.sendBackward() app.sendBackward()
}, },
@ -607,7 +615,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'send-to-back', icon: 'send-to-back',
kbd: '[', kbd: '[',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'reorder-shapes', { operation: 'toBack' })
app.mark('send to back') app.mark('send to back')
app.sendToBack() app.sendToBack()
}, },
@ -617,7 +626,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.cut', label: 'action.cut',
kbd: '$x', kbd: '$x',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
trackEvent(source, 'cut')
app.mark('cut') app.mark('cut')
cut() cut()
}, },
@ -627,7 +637,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.copy', label: 'action.copy',
kbd: '$c', kbd: '$c',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'copy')
copy() copy()
}, },
}, },
@ -646,7 +657,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.select-all', label: 'action.select-all',
kbd: '$a', kbd: '$a',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'select-all-shapes')
if (app.currentToolId !== 'select') { if (app.currentToolId !== 'select') {
app.cancel() app.cancel()
app.setSelectedTool('select') app.setSelectedTool('select')
@ -660,7 +672,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
id: 'select-none', id: 'select-none',
label: 'action.select-none', label: 'action.select-none',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'select-none-shapes')
app.mark('select none') app.mark('select none')
app.selectNone() app.selectNone()
}, },
@ -671,8 +684,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '⌫', kbd: '⌫',
icon: 'trash', icon: 'trash',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
if (app.currentToolId !== 'select') return if (app.currentToolId !== 'select') return
trackEvent(source, 'delete-shapes')
app.mark('delete') app.mark('delete')
app.deleteShapes() app.deleteShapes()
}, },
@ -682,8 +696,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.rotate-cw', label: 'action.rotate-cw',
icon: 'rotate-cw', icon: 'rotate-cw',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
if (app.selectedIds.length === 0) return if (app.selectedIds.length === 0) return
trackEvent(source, 'rotate-cw')
app.mark('rotate-cw') app.mark('rotate-cw')
const offset = app.selectionRotation % (TAU / 2) const offset = app.selectionRotation % (TAU / 2)
const dontUseOffset = approximately(offset, 0) || approximately(offset, TAU / 2) const dontUseOffset = approximately(offset, 0) || approximately(offset, TAU / 2)
@ -695,8 +710,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.rotate-ccw', label: 'action.rotate-ccw',
icon: 'rotate-ccw', icon: 'rotate-ccw',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
if (app.selectedIds.length === 0) return if (app.selectedIds.length === 0) return
trackEvent(source, 'rotate-ccw')
app.mark('rotate-ccw') app.mark('rotate-ccw')
const offset = app.selectionRotation % (TAU / 2) const offset = app.selectionRotation % (TAU / 2)
const offsetCloseToZero = approximately(offset, 0) const offsetCloseToZero = approximately(offset, 0)
@ -708,7 +724,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.zoom-in', label: 'action.zoom-in',
kbd: '$=', kbd: '$=',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'zoom-in')
app.zoomIn(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS }) app.zoomIn(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS })
}, },
}, },
@ -717,7 +734,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.zoom-out', label: 'action.zoom-out',
kbd: '$-', kbd: '$-',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'zoom-out')
app.zoomOut(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS }) app.zoomOut(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS })
}, },
}, },
@ -727,7 +745,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
icon: 'reset-zoom', icon: 'reset-zoom',
kbd: '!0', kbd: '!0',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'reset-zoom')
app.resetZoom(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS }) app.resetZoom(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS })
}, },
}, },
@ -736,7 +755,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.zoom-to-fit', label: 'action.zoom-to-fit',
kbd: '!1', kbd: '!1',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'zoom-to-fit')
app.zoomToFit({ duration: ANIMATION_MEDIUM_MS }) app.zoomToFit({ duration: ANIMATION_MEDIUM_MS })
}, },
}, },
@ -745,7 +765,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.zoom-to-selection', label: 'action.zoom-to-selection',
kbd: '!2', kbd: '!2',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'zoom-to-selection')
app.zoomToSelection({ duration: ANIMATION_MEDIUM_MS }) app.zoomToSelection({ duration: ANIMATION_MEDIUM_MS })
}, },
}, },
@ -754,13 +775,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.toggle-snap-mode', label: 'action.toggle-snap-mode',
menuLabel: 'action.toggle-snap-mode.menu', menuLabel: 'action.toggle-snap-mode.menu',
readonlyOk: false, readonlyOk: false,
onSelect() { onSelect(source) {
app.updateUserDocumentSettings( trackEvent(source, 'toggle-snap-mode')
{ app.setSnapMode(!app.isSnapMode)
isSnapMode: !app.userDocumentSettings.isSnapMode,
},
true
)
}, },
checkbox: true, checkbox: true,
}, },
@ -770,13 +787,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.toggle-dark-mode.menu', menuLabel: 'action.toggle-dark-mode.menu',
kbd: '$/', kbd: '$/',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
app.updateUserDocumentSettings( trackEvent(source, 'toggle-dark-mode')
{ app.setDarkMode(!app.isDarkMode)
isDarkMode: !app.userDocumentSettings.isDarkMode,
},
true
)
}, },
checkbox: true, checkbox: true,
}, },
@ -786,7 +799,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.toggle-transparent.menu', menuLabel: 'action.toggle-transparent.menu',
contextMenuLabel: 'action.toggle-transparent.context-menu', contextMenuLabel: 'action.toggle-transparent.context-menu',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'toggle-transparent')
app.updateInstanceState( app.updateInstanceState(
{ {
exportBackground: !app.instanceState.exportBackground, exportBackground: !app.instanceState.exportBackground,
@ -802,13 +816,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.toggle-tool-lock.menu', menuLabel: 'action.toggle-tool-lock.menu',
readonlyOk: false, readonlyOk: false,
kbd: 'q', kbd: 'q',
onSelect() { onSelect(source) {
app.updateInstanceState( trackEvent(source, 'toggle-tool-lock')
{ app.setToolLocked(!app.isToolLocked)
isToolLocked: !app.instanceState.isToolLocked,
},
true
)
}, },
checkbox: true, checkbox: true,
}, },
@ -819,19 +829,15 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true, readonlyOk: true,
kbd: '$.', kbd: '$.',
checkbox: true, checkbox: true,
onSelect() { onSelect(source) {
// this needs to be deferred because it causes the menu // this needs to be deferred because it causes the menu
// UI to unmount which puts us in a dodgy state // UI to unmount which puts us in a dodgy state
requestAnimationFrame(() => { requestAnimationFrame(() => {
app.batch(() => { app.batch(() => {
trackEvent(source, 'toggle-focus-mode')
clearDialogs() clearDialogs()
clearToasts() clearToasts()
app.updateInstanceState( app.setFocusMode(!app.isFocusMode)
{
isFocusMode: !app.instanceState.isFocusMode,
},
true
)
}) })
}) })
}, },
@ -842,13 +848,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
menuLabel: 'action.toggle-grid.menu', menuLabel: 'action.toggle-grid.menu',
readonlyOk: true, readonlyOk: true,
kbd: "$'", kbd: "$'",
onSelect() { onSelect(source) {
app.updateUserDocumentSettings( trackEvent(source, 'toggle-grid-mode')
{ app.setGridMode(!app.isGridMode)
isGridMode: !app.userDocumentSettings.isGridMode,
},
true
)
}, },
checkbox: true, checkbox: true,
}, },
@ -857,7 +859,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.toggle-debug-mode', label: 'action.toggle-debug-mode',
menuLabel: 'action.toggle-debug-mode.menu', menuLabel: 'action.toggle-debug-mode.menu',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'toggle-debug-mode')
app.updateInstanceState( app.updateInstanceState(
{ {
isDebugMode: !app.instanceState.isDebugMode, isDebugMode: !app.instanceState.isDebugMode,
@ -872,7 +875,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.print', label: 'action.print',
kbd: '$p', kbd: '$p',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'print')
printSelectionOrPages() printSelectionOrPages()
}, },
}, },
@ -881,7 +885,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.exit-pen-mode', label: 'action.exit-pen-mode',
icon: 'cross-2', icon: 'cross-2',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'exit-pen-mode')
app.setPenMode(false) app.setPenMode(false)
}, },
}, },
@ -890,7 +895,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.stop-following', label: 'action.stop-following',
icon: 'cross-2', icon: 'cross-2',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
trackEvent(source, 'stop-following')
app.stopFollowingUser() app.stopFollowingUser()
}, },
}, },
@ -899,19 +905,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
label: 'action.back-to-content', label: 'action.back-to-content',
icon: 'arrow-left', icon: 'arrow-left',
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect(source) {
const bounds = app.selectedPageBounds ?? app.allShapesCommonBounds trackEvent(source, 'zoom-to-content')
app.zoomToContent()
if (bounds) {
app.zoomToBounds(
bounds.minX,
bounds.minY,
bounds.width,
bounds.height,
Math.min(1, app.zoomLevel),
{ duration: 220 }
)
}
}, },
}, },
]) ])
@ -922,6 +918,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
return actions return actions
}, [ }, [
trackEvent,
overrides, overrides,
app, app,
addDialog, addDialog,

Wyświetl plik

@ -1,4 +1,4 @@
import { TLPageId, useApp } from '@tldraw/editor' import { useApp } from '@tldraw/editor'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useToasts } from './useToastsProvider' import { useToasts } from './useToastsProvider'
@ -15,28 +15,9 @@ export function useAppEvents() {
}) })
} }
function handleMoveToPage({ name, fromId }: { name: string; fromId: TLPageId }) {
addToast({
title: 'Changed Page',
description: `Moved to ${name}.`,
actions: [
{
label: 'Go Back',
type: 'primary',
onClick: () => {
app.mark('change-page')
app.setCurrentPageId(fromId)
},
}, // prev page
],
})
}
app.addListener('max-shapes', handleMaxShapes) app.addListener('max-shapes', handleMaxShapes)
app.addListener('moved-to-page', handleMoveToPage)
return () => { return () => {
app.removeListener('max-shapes', handleMaxShapes) app.removeListener('max-shapes', handleMaxShapes)
app.removeListener('moved-to-page', handleMoveToPage)
} }
}, [app, addToast]) }, [app, addToast])
} }

Wyświetl plik

@ -40,6 +40,7 @@ import { compact, isNonNull } from '@tldraw/utils'
import { compressToBase64, decompressFromBase64 } from 'lz-string' import { compressToBase64, decompressFromBase64 } from 'lz-string'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { useAppIsFocused } from './useAppIsFocused' import { useAppIsFocused } from './useAppIsFocused'
import { useEvents } from './useEventsProvider'
/** @public */ /** @public */
export type EmbedInfo = { export type EmbedInfo = {
@ -969,22 +970,27 @@ const handleNativeClipboardPaste = async (
/** @public */ /** @public */
export function useMenuClipboardEvents() { export function useMenuClipboardEvents() {
const app = useApp() const app = useApp()
const trackEvent = useEvents()
const copy = useCallback( const copy = useCallback(
function onCopy() { function onCopy() {
if (app.selectedIds.length === 0) return if (app.selectedIds.length === 0) return
handleMenuCopy(app) handleMenuCopy(app)
trackEvent('menu', 'copy')
}, },
[app] [app, trackEvent]
) )
const cut = useCallback( const cut = useCallback(
function onCut() { function onCut() {
if (app.selectedIds.length === 0) return if (app.selectedIds.length === 0) return
handleMenuCopy(app) handleMenuCopy(app)
app.deleteShapes() app.deleteShapes()
trackEvent('menu', 'cut')
}, },
[app] [app, trackEvent]
) )
const paste = useCallback( const paste = useCallback(
@ -1000,8 +1006,10 @@ export function useMenuClipboardEvents() {
// else { // else {
// handleScenePaste(app, point) // handleScenePaste(app, point)
// } // }
trackEvent('menu', 'paste')
}, },
[app] [app, trackEvent]
) )
return { return {
@ -1014,6 +1022,7 @@ export function useMenuClipboardEvents() {
/** @public */ /** @public */
export function useNativeClipboardEvents() { export function useNativeClipboardEvents() {
const app = useApp() const app = useApp()
const trackEvent = useEvents()
const appIsFocused = useAppIsFocused() const appIsFocused = useAppIsFocused()
@ -1023,6 +1032,7 @@ export function useNativeClipboardEvents() {
if (app.selectedIds.length === 0 || app.editingId !== null || disallowClipboardEvents(app)) if (app.selectedIds.length === 0 || app.editingId !== null || disallowClipboardEvents(app))
return return
handleMenuCopy(app) handleMenuCopy(app)
trackEvent('kbd', 'copy')
} }
function cut() { function cut() {
@ -1030,12 +1040,13 @@ export function useNativeClipboardEvents() {
return return
handleMenuCopy(app) handleMenuCopy(app)
app.deleteShapes() app.deleteShapes()
trackEvent('kbd', 'cut')
} }
const paste = (event: ClipboardEvent) => { const paste = (e: ClipboardEvent) => {
if (app.editingId !== null || disallowClipboardEvents(app)) return if (app.editingId !== null || disallowClipboardEvents(app)) return
if (event.clipboardData && !app.inputs.shiftKey) { if (e.clipboardData && !app.inputs.shiftKey) {
handleNativeDataTransferPaste(app, event.clipboardData) handleNativeDataTransferPaste(app, e.clipboardData)
} else { } else {
navigator.clipboard.read().then((clipboardItems) => { navigator.clipboard.read().then((clipboardItems) => {
if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) { if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) {
@ -1043,6 +1054,7 @@ export function useNativeClipboardEvents() {
} }
}) })
} }
trackEvent('kbd', 'paste')
} }
document.addEventListener('copy', copy) document.addEventListener('copy', copy)
@ -1054,5 +1066,5 @@ export function useNativeClipboardEvents() {
document.removeEventListener('cut', cut) document.removeEventListener('cut', cut)
document.removeEventListener('paste', paste) document.removeEventListener('paste', paste)
} }
}, [app, appIsFocused]) }, [app, trackEvent, appIsFocused])
} }

Wyświetl plik

@ -1,5 +1,6 @@
import { App, uniqueId, useApp } from '@tldraw/editor' import { App, uniqueId, useApp } from '@tldraw/editor'
import { createContext, useCallback, useContext, useState } from 'react' import { createContext, useCallback, useContext, useState } from 'react'
import { useEvents } from './useEventsProvider'
/** @public */ /** @public */
export interface DialogProps { export interface DialogProps {
@ -34,6 +35,7 @@ export type DialogsProviderProps = {
/** @public */ /** @public */
export function DialogsProvider({ children }: DialogsProviderProps) { export function DialogsProvider({ children }: DialogsProviderProps) {
const app = useApp() const app = useApp()
const trackEvent = useEvents()
const [dialogs, setDialogs] = useState<TLDialog[]>([]) const [dialogs, setDialogs] = useState<TLDialog[]>([])
@ -44,11 +46,12 @@ export function DialogsProvider({ children }: DialogsProviderProps) {
return [...d.filter((m) => m.id !== dialog.id), { ...dialog, id }] return [...d.filter((m) => m.id !== dialog.id), { ...dialog, id }]
}) })
app.openMenus.add(id) trackEvent('dialog', 'open-menu', { id })
app.addOpenMenu(id)
return id return id
}, },
[app] [app, trackEvent]
) )
const updateDialog = useCallback( const updateDialog = useCallback(
@ -65,11 +68,12 @@ export function DialogsProvider({ children }: DialogsProviderProps) {
}) })
) )
app.openMenus.add(id) trackEvent('dialog', 'open-menu', { id })
app.addOpenMenu(id)
return id return id
}, },
[app] [app, trackEvent]
) )
const removeDialog = useCallback( const removeDialog = useCallback(
@ -84,22 +88,24 @@ export function DialogsProvider({ children }: DialogsProviderProps) {
}) })
) )
app.openMenus.delete(id) trackEvent('dialog', 'close-menu', { id })
app.deleteOpenMenu(id)
return id return id
}, },
[app] [app, trackEvent]
) )
const clearDialogs = useCallback(() => { const clearDialogs = useCallback(() => {
setDialogs((d) => { setDialogs((d) => {
d.forEach((m) => { d.forEach((m) => {
m.onClose?.() m.onClose?.()
app.openMenus.delete(m.id) trackEvent('dialog', 'close-menu', { id: m.id })
app.deleteOpenMenu(m.id)
}) })
return [] return []
}) })
}, [app]) }, [app, trackEvent])
return ( return (
<DialogsContext.Provider <DialogsContext.Provider

Wyświetl plik

@ -0,0 +1,113 @@
import * as React from 'react'
/** @public */
export type TLUiEventSource =
| 'menu'
| 'context-menu'
| 'zoom-menu'
| 'navigation-zone'
| 'quick-actions'
| 'actions-menu'
| 'kbd'
| 'debug-panel'
| 'page-menu'
| 'share-menu'
| 'toolbar'
| 'people-menu'
| 'dialog'
| 'help-menu'
| 'helper-buttons'
/** @public */
export interface TLUiEventMap {
// Actions
undo: undefined
redo: undefined
'group-shapes': undefined
'ungroup-shapes': undefined
'convert-to-embed': undefined
'convert-to-bookmark': undefined
'open-embed-link': undefined
'toggle-auto-size': undefined
'copy-as': { format: 'svg' | 'png' | 'json' }
'export-as': { format: 'svg' | 'png' | 'json' }
'edit-link': undefined
'insert-embed': undefined
'insert-media': undefined
'align-shapes': {
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom'
}
'duplicate-shapes': undefined
'pack-shapes': undefined
'stack-shapes': { operation: 'horizontal' | 'vertical' }
'flip-shapes': { operation: 'horizontal' | 'vertical' }
'distribute-shapes': { operation: 'horizontal' | 'vertical' }
'stretch-shapes': { operation: 'horizontal' | 'vertical' }
'reorder-shapes': {
operation: 'toBack' | 'toFront' | 'forward' | 'backward'
}
'delete-shapes': undefined
'select-all-shapes': undefined
'select-none-shapes': undefined
'rotate-ccw': undefined
'rotate-cw': undefined
'zoom-in': undefined
'zoom-out': undefined
'zoom-to-fit': undefined
'zoom-to-selection': undefined
'reset-zoom': undefined
'zoom-into-view': undefined
'zoom-to-content': undefined
'open-menu': { id: string }
'close-menu': { id: string }
'create-new-project': undefined
'save-project-to-file': undefined
'open-file': undefined
'select-tool': { id: string }
print: undefined
copy: undefined
paste: undefined
cut: undefined
'toggle-transparent': undefined
'toggle-snap-mode': undefined
'toggle-tool-lock': undefined
'toggle-grid-mode': undefined
'toggle-dark-mode': undefined
'toggle-focus-mode': undefined
'toggle-debug-mode': undefined
'exit-pen-mode': undefined
'stop-following': undefined
}
/** @public */
export type TLUiEventHandler<T extends keyof TLUiEventMap = keyof TLUiEventMap> = (
source: string,
name: T,
data?: TLUiEventMap[T]
) => void
/** @internal */
const defaultEventHandler: TLUiEventHandler = () => void null
/** @internal */
export const EventsContext = React.createContext({} as TLUiEventHandler)
/** @public */
export type EventsProviderProps = {
onEvent?: TLUiEventHandler
children: any
}
/** @public */
export function EventsProvider({ onEvent, children }: EventsProviderProps) {
return (
<EventsContext.Provider value={onEvent ?? defaultEventHandler}>
{children}
</EventsContext.Provider>
)
}
/** @public */
export function useEvents(): TLUiEventHandler<keyof TLUiEventMap> {
return React.useContext(EventsContext)
}

Wyświetl plik

@ -72,6 +72,7 @@ export function useExportAs() {
const dataURL = URL.createObjectURL(image) const dataURL = URL.createObjectURL(image)
downloadDataURLAsFile(dataURL, `${name || 'shapes'}.${format}`) downloadDataURLAsFile(dataURL, `${name || 'shapes'}.${format}`)
URL.revokeObjectURL(dataURL) URL.revokeObjectURL(dataURL)
return return
} }
@ -83,6 +84,7 @@ export function useExportAs() {
) )
downloadDataURLAsFile(dataURL, `${name || 'shapes'}.json`) downloadDataURLAsFile(dataURL, `${name || 'shapes'}.json`)
URL.revokeObjectURL(dataURL) URL.revokeObjectURL(dataURL)
return return
} }

Wyświetl plik

@ -11,6 +11,7 @@ const SKIP_KBDS = [
'copy', 'copy',
'cut', 'cut',
'paste', 'paste',
'delete',
// There's also an upload asset action, so we don't want to set the kbd twice // There's also an upload asset action, so we don't want to set the kbd twice
'asset', 'asset',
] ]
@ -45,7 +46,7 @@ export function useKeyboardShortcuts() {
hot(getHotkeysStringFromKbd(action.kbd), (event) => { hot(getHotkeysStringFromKbd(action.kbd), (event) => {
if (areShortcutsDisabled()) return if (areShortcutsDisabled()) return
preventDefault(event) preventDefault(event)
action.onSelect() action.onSelect('kbd')
}) })
} }
@ -69,19 +70,19 @@ export function useKeyboardShortcuts() {
app.setSelectedTool('geo') app.setSelectedTool('geo')
}) })
hot('backspace,del', () => { hot('del,backspace', () => {
if (areShortcutsDisabled()) return if (areShortcutsDisabled()) return
actions['delete'].onSelect() actions['delete'].onSelect('kbd')
}) })
hot('=', () => { hot('=', () => {
if (areShortcutsDisabled()) return if (areShortcutsDisabled()) return
actions['zoom-in'].onSelect() actions['zoom-in'].onSelect('kbd')
}) })
hot('-', () => { hot('-', () => {
if (areShortcutsDisabled()) return if (areShortcutsDisabled()) return
actions['zoom-out'].onSelect() actions['zoom-out'].onSelect('kbd')
}) })
hotkeys.setScope(app.instanceId) hotkeys.setScope(app.instanceId)

Wyświetl plik

@ -1,27 +1,31 @@
import { useApp } from '@tldraw/editor' import { useApp } from '@tldraw/editor'
import { useCallback, useEffect, useRef } from 'react' import { useCallback, useEffect, useRef } from 'react'
import { useEvents } from './useEventsProvider'
/** @public */ /** @public */
export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) { export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
const app = useApp() const app = useApp()
const rIsOpen = useRef(false) const rIsOpen = useRef(false)
const trackEvent = useEvents()
const onOpenChange = useCallback( const onOpenChange = useCallback(
(isOpen: boolean) => { (isOpen: boolean) => {
rIsOpen.current = isOpen rIsOpen.current = isOpen
app.batch(() => {
if (isOpen) { if (isOpen) {
app.complete() app.complete()
app.openMenus.add(id) app.addOpenMenu(id)
} else { } else {
app.openMenus.delete(id) app.deleteOpenMenu(id)
app.openMenus.forEach((menuId) => { app.openMenus.forEach((menuId) => {
if (menuId.startsWith(id)) { if (menuId.startsWith(id)) {
app.openMenus.delete(menuId) app.deleteOpenMenu(menuId)
} }
}) })
} }
cb?.(isOpen) cb?.(isOpen)
})
}, },
[app, id, cb] [app, id, cb]
) )
@ -36,25 +40,27 @@ export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
// hook but it's necessary to handle the case where the // hook but it's necessary to handle the case where the
// this effect runs twice or re-runs. // this effect runs twice or re-runs.
if (rIsOpen.current) { if (rIsOpen.current) {
app.openMenus.add(id) trackEvent('menu', 'open-menu', { id })
app.addOpenMenu(id)
} }
return () => { return () => {
if (rIsOpen.current) { if (rIsOpen.current) {
// Close menu on unmount // Close menu on unmount
app.openMenus.delete(id) app.deleteOpenMenu(id)
// Close menu and all submenus when the parent is closed // Close menu and all submenus when the parent is closed
app.openMenus.forEach((menuId) => { app.openMenus.forEach((menuId) => {
if (menuId.startsWith(id)) { if (menuId.startsWith(id)) {
app.openMenus.delete(menuId) trackEvent('menu', 'close-menu', { id })
app.deleteOpenMenu(menuId)
} }
}) })
rIsOpen.current = false rIsOpen.current = false
} }
} }
}, [app, id]) }, [app, id, trackEvent])
return onOpenChange return onOpenChange
} }

Wyświetl plik

@ -129,8 +129,7 @@ export function usePrint() {
} }
const afterPrintHandler = () => { const afterPrintHandler = () => {
// TODO: This is kind of lazy at the moment. I guess we need an event for 'something-happens-on-canvas' app.once('change-history', () => {
app.once('change-camera', () => {
clearElements(el, style) clearElements(el, style)
}) })
} }

Wyświetl plik

@ -3,6 +3,7 @@ import * as React from 'react'
import { EmbedDialog } from '../components/EmbedDialog' import { EmbedDialog } from '../components/EmbedDialog'
import { TLUiIconType } from '../icon-types' import { TLUiIconType } from '../icon-types'
import { useDialogs } from './useDialogsProvider' import { useDialogs } from './useDialogsProvider'
import { useEvents } from './useEventsProvider'
import { useInsertMedia } from './useInsertMedia' import { useInsertMedia } from './useInsertMedia'
import { TLTranslationKey } from './useTranslation/TLTranslationKey' import { TLTranslationKey } from './useTranslation/TLTranslationKey'
@ -39,6 +40,7 @@ export type ToolsProviderProps = {
/** @public */ /** @public */
export function ToolsProvider({ overrides, children }: ToolsProviderProps) { export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
const app = useApp() const app = useApp()
const trackEvent = useEvents()
const { addDialog } = useDialogs() const { addDialog } = useDialogs()
const insertMedia = useInsertMedia() const insertMedia = useInsertMedia()
@ -53,6 +55,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect() {
app.setSelectedTool('select') app.setSelectedTool('select')
trackEvent('toolbar', 'select-tool', { id: 'select' })
}, },
}, },
{ {
@ -63,6 +66,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect() {
app.setSelectedTool('hand') app.setSelectedTool('hand')
trackEvent('toolbar', 'select-tool', { id: 'hand' })
}, },
}, },
{ {
@ -73,6 +77,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
readonlyOk: true, readonlyOk: true,
onSelect() { onSelect() {
app.setSelectedTool('eraser') app.setSelectedTool('eraser')
trackEvent('toolbar', 'select-tool', { id: 'eraser' })
}, },
}, },
{ {
@ -83,6 +88,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: 'd,b,x', kbd: 'd,b,x',
onSelect() { onSelect() {
app.setSelectedTool('draw') app.setSelectedTool('draw')
trackEvent('toolbar', 'select-tool', { id: 'draw' })
}, },
}, },
...[...TL_GEO_TYPES].map((id) => ({ ...[...TL_GEO_TYPES].map((id) => ({
@ -101,6 +107,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
true true
) )
app.setSelectedTool('geo') app.setSelectedTool('geo')
trackEvent('toolbar', 'select-tool', { id: `geo-${id}` })
}) })
}, },
})), })),
@ -112,6 +119,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: 'a', kbd: 'a',
onSelect() { onSelect() {
app.setSelectedTool('arrow') app.setSelectedTool('arrow')
trackEvent('toolbar', 'select-tool', { id: 'arrow' })
}, },
}, },
{ {
@ -122,6 +130,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: 'l', kbd: 'l',
onSelect() { onSelect() {
app.setSelectedTool('line') app.setSelectedTool('line')
trackEvent('toolbar', 'select-tool', { id: 'line' })
}, },
}, },
{ {
@ -132,6 +141,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: 'f', kbd: 'f',
onSelect() { onSelect() {
app.setSelectedTool('frame') app.setSelectedTool('frame')
trackEvent('toolbar', 'select-tool', { id: 'frame' })
}, },
}, },
{ {
@ -142,6 +152,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: 't', kbd: 't',
onSelect() { onSelect() {
app.setSelectedTool('text') app.setSelectedTool('text')
trackEvent('toolbar', 'select-tool', { id: 'text' })
}, },
}, },
{ {
@ -152,6 +163,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: '$u', kbd: '$u',
onSelect() { onSelect() {
insertMedia() insertMedia()
trackEvent('toolbar', 'select-tool', { id: 'media' })
}, },
}, },
{ {
@ -162,6 +174,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
kbd: 'n', kbd: 'n',
onSelect() { onSelect() {
app.setSelectedTool('note') app.setSelectedTool('note')
trackEvent('toolbar', 'select-tool', { id: 'note' })
}, },
}, },
{ {
@ -171,6 +184,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
icon: 'tool-embed', icon: 'tool-embed',
onSelect() { onSelect() {
addDialog({ component: EmbedDialog }) addDialog({ component: EmbedDialog })
trackEvent('toolbar', 'select-tool', { id: 'embed' })
}, },
}, },
]) ])
@ -180,7 +194,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
} }
return tools return tools
}, [app, overrides, insertMedia, addDialog]) }, [app, trackEvent, overrides, insertMedia, addDialog])
return <ToolsContext.Provider value={tools}>{children}</ToolsContext.Provider> return <ToolsContext.Provider value={tools}>{children}</ToolsContext.Provider>
} }

Wyświetl plik

@ -5891,6 +5891,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vercel/analytics@npm:^1.0.1":
version: 1.0.1
resolution: "@vercel/analytics@npm:1.0.1"
checksum: 6876e1d0868e85198d43a17ffec7266cdee5a53d5fc797d3f7e0d64e9d76a2ade3cbdcbc0a665e2ee2389f65dfc80591f7d667e9fc981cdbaa848006cbaefe19
languageName: node
linkType: hard
"@vercel/build-utils@npm:6.7.1": "@vercel/build-utils@npm:6.7.1":
version: 6.7.1 version: 6.7.1
resolution: "@vercel/build-utils@npm:6.7.1" resolution: "@vercel/build-utils@npm:6.7.1"
@ -10819,6 +10826,7 @@ __metadata:
"@babel/plugin-proposal-decorators": ^7.21.0 "@babel/plugin-proposal-decorators": ^7.21.0
"@tldraw/assets": "workspace:*" "@tldraw/assets": "workspace:*"
"@tldraw/tldraw": "workspace:*" "@tldraw/tldraw": "workspace:*"
"@vercel/analytics": ^1.0.1
lazyrepo: 0.0.0-alpha.26 lazyrepo: 0.0.0-alpha.26
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0