kopia lustrzana https://github.com/Tldraw/Tldraw
[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
rodzic
5061240912
commit
3437ca89d9
|
@ -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
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 />,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,27 +1549,29 @@ 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 {
|
||||||
this.updateUserDocumentSettings({ isGridMode }, true)
|
if (isGridMode === this.isGridMode) {
|
||||||
}
|
this.updateUserDocumentSettings({ isGridMode }, true)
|
||||||
|
|
||||||
setDarkMode(isDarkMode: boolean) {
|
|
||||||
this.updateUserDocumentSettings({ isDarkMode }, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
setReadOnly(isReadOnly: boolean) {
|
|
||||||
this.updateUserDocumentSettings({ isReadOnly }, true)
|
|
||||||
if (isReadOnly) {
|
|
||||||
this.setSelectedTool('hand')
|
|
||||||
}
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
get isReadOnly() {
|
||||||
|
return this.userDocumentSettings.isReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
setReadOnly(isReadOnly: boolean): this {
|
||||||
|
if (isReadOnly !== this.isReadOnly) {
|
||||||
|
this.updateUserDocumentSettings({ isReadOnly }, true)
|
||||||
|
if (isReadOnly) {
|
||||||
|
this.setSelectedTool('hand')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -1494,10 +1584,12 @@ 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,31 +7804,33 @@ 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 }
|
||||||
|
|
||||||
const { currentScreenPoint } = this.inputs
|
this.batch(() => {
|
||||||
|
this.store.put([nextCamera])
|
||||||
|
|
||||||
this.dispatch({
|
const { currentScreenPoint } = this.inputs
|
||||||
type: 'pointer',
|
|
||||||
target: 'canvas',
|
this.dispatch({
|
||||||
name: 'pointer_move',
|
type: 'pointer',
|
||||||
point: currentScreenPoint,
|
target: 'canvas',
|
||||||
pointerId: 0,
|
name: 'pointer_move',
|
||||||
ctrlKey: this.inputs.ctrlKey,
|
point: currentScreenPoint,
|
||||||
altKey: this.inputs.altKey,
|
pointerId: 0,
|
||||||
shiftKey: this.inputs.shiftKey,
|
ctrlKey: this.inputs.ctrlKey,
|
||||||
button: 0,
|
altKey: this.inputs.altKey,
|
||||||
isPen: this.isPenMode ?? false,
|
shiftKey: this.inputs.shiftKey,
|
||||||
|
button: 0,
|
||||||
|
isPen: this.isPenMode ?? false,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.updateUserPresence({
|
||||||
|
viewportPageBounds: this.viewportPageBounds.toJson(),
|
||||||
|
})
|
||||||
|
|
||||||
|
this._cameraManager.tick()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.updateUserPresence({
|
|
||||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
|
||||||
})
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
||||||
/** @public */
|
|
||||||
export type TLReorderOperation = 'toBack' | 'toFront' | 'forward' | 'backward'
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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' })
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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>
|
||||||
<InternalProviders overrides={overrides}>{children}</InternalProviders>
|
<EventsProvider onEvent={onEvent}>
|
||||||
|
<InternalProviders overrides={overrides}>{children}</InternalProviders>
|
||||||
|
</EventsProvider>
|
||||||
</BreakPointProvider>
|
</BreakPointProvider>
|
||||||
</DialogsProvider>
|
</DialogsProvider>
|
||||||
</ToastsProvider>
|
</ToastsProvider>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 } }) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
if (isOpen) {
|
app.batch(() => {
|
||||||
app.complete()
|
if (isOpen) {
|
||||||
app.openMenus.add(id)
|
app.complete()
|
||||||
} else {
|
app.addOpenMenu(id)
|
||||||
app.openMenus.delete(id)
|
} else {
|
||||||
app.openMenus.forEach((menuId) => {
|
app.deleteOpenMenu(id)
|
||||||
if (menuId.startsWith(id)) {
|
app.openMenus.forEach((menuId) => {
|
||||||
app.openMenus.delete(menuId)
|
if (menuId.startsWith(id)) {
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Ładowanie…
Reference in New Issue