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",
|
||||
"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": {
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"lazyrepo": "0.0.0-alpha.26",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"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 CustomComponentsExample from './10-custom-components/CustomComponentsExample'
|
||||
import UserPresenceExample from './11-user-presence/UserPresenceExample'
|
||||
import EventsExample from './12-events/EventsExample'
|
||||
import ExampleApi from './2-api/APIExample'
|
||||
import CustomConfigExample from './3-custom-config/CustomConfigExample'
|
||||
import CustomUiExample from './4-custom-ui/CustomUiExample'
|
||||
|
@ -71,6 +72,10 @@ export const allExamples: Example[] = [
|
|||
path: '/custom-components',
|
||||
element: <CustomComponentsExample />,
|
||||
},
|
||||
{
|
||||
path: '/events',
|
||||
element: <EventsExample />,
|
||||
},
|
||||
{
|
||||
path: '/user-presence',
|
||||
element: <UserPresenceExample />,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
/// <reference types="react" />
|
||||
|
||||
import { Atom } from 'signia';
|
||||
import { BaseRecord } from '@tldraw/tlstore';
|
||||
import { Box2d } from '@tldraw/primitives';
|
||||
import { Box2dModel } from '@tldraw/tlschema';
|
||||
import { Computed } from 'signia';
|
||||
|
@ -120,8 +121,9 @@ export type AnimationOptions = Partial<{
|
|||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export class App extends EventEmitter {
|
||||
export class App extends EventEmitter<TLEventMap> {
|
||||
constructor({ config, store, getContainer }: AppOptions);
|
||||
addOpenMenu: (id: string) => this;
|
||||
alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this;
|
||||
get allShapesCommonBounds(): Box2d | null;
|
||||
animateCamera(x: number, y: number, z?: number, opts?: AnimationOptions): this;
|
||||
|
@ -193,6 +195,7 @@ export class App extends EventEmitter {
|
|||
// (undocumented)
|
||||
get cursor(): TLCursor;
|
||||
deleteAssets(ids: TLAssetId[]): this;
|
||||
deleteOpenMenu: (id: string) => this;
|
||||
deletePage(id: TLPageId): void;
|
||||
deleteShapes(ids?: TLShapeId[]): this;
|
||||
deselect(...ids: TLShapeId[]): this;
|
||||
|
@ -326,8 +329,12 @@ export class App extends EventEmitter {
|
|||
readonly isChromeForIos: boolean;
|
||||
get isCoarsePointer(): boolean;
|
||||
set isCoarsePointer(v: boolean);
|
||||
// (undocumented)
|
||||
get isDarkMode(): boolean;
|
||||
get isFocused(): boolean;
|
||||
// (undocumented)
|
||||
get isFocusMode(): boolean;
|
||||
// (undocumented)
|
||||
get isGridMode(): boolean;
|
||||
isIn(path: string): boolean;
|
||||
isInAny(...paths: string[]): boolean;
|
||||
|
@ -342,6 +349,10 @@ export class App extends EventEmitter {
|
|||
isSelected(id: TLShapeId): boolean;
|
||||
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
|
||||
isShapeInViewport(id: TLShapeId): boolean;
|
||||
// (undocumented)
|
||||
get isSnapMode(): boolean;
|
||||
// (undocumented)
|
||||
get isToolLocked(): boolean;
|
||||
isWithinSelection(id: TLShapeId): boolean;
|
||||
// (undocumented)
|
||||
lockShapes(_ids?: TLShapeId[]): this;
|
||||
|
@ -355,7 +366,7 @@ export class App extends EventEmitter {
|
|||
description: string;
|
||||
}>;
|
||||
get onlySelectedShape(): TLBaseShape<any, any> | null;
|
||||
openMenus: Set<string>;
|
||||
get openMenus(): string[];
|
||||
packShapes(ids?: TLShapeId[], padding?: number): this;
|
||||
get pages(): TLPage[];
|
||||
get pageState(): TLInstancePageState;
|
||||
|
@ -386,7 +397,7 @@ export class App extends EventEmitter {
|
|||
isCulled: 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;
|
||||
// (undocumented)
|
||||
replaceStoreContentsWithRecordsForOtherDocument(records: TLRecord[]): void;
|
||||
|
@ -431,23 +442,29 @@ export class App extends EventEmitter {
|
|||
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: ViewportOptions): this;
|
||||
setCursor(cursor: Partial<TLCursor>): this;
|
||||
// (undocumented)
|
||||
setDarkMode(isDarkMode: boolean): void;
|
||||
setDarkMode(isDarkMode: boolean): this;
|
||||
setEditingId(id: null | TLShapeId): this;
|
||||
setErasingIds(ids?: TLShapeId[]): this;
|
||||
setFocusLayer(next: null | TLShapeId): this;
|
||||
// (undocumented)
|
||||
setGridMode(isGridMode: boolean): void;
|
||||
setFocusMode(isFocusMode: boolean): this;
|
||||
// (undocumented)
|
||||
setGridMode(isGridMode: boolean): this;
|
||||
setHintingIds(ids: TLShapeId[]): this;
|
||||
setHoveredId(id?: null | TLShapeId): this;
|
||||
setInstancePageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
|
||||
// (undocumented)
|
||||
setPenMode(isPenMode: boolean): void;
|
||||
setPenMode(isPenMode: boolean): this;
|
||||
setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this;
|
||||
// (undocumented)
|
||||
setReadOnly(isReadOnly: boolean): void;
|
||||
setReadOnly(isReadOnly: boolean): this;
|
||||
setScribble(scribble?: null | TLScribble): this;
|
||||
setSelectedIds(ids: TLShapeId[], squashing?: boolean): this;
|
||||
setSelectedTool(id: string, info?: {}): this;
|
||||
// (undocumented)
|
||||
setSnapMode(isSnapMode: boolean): this;
|
||||
// (undocumented)
|
||||
setToolLocked(isToolLocked: boolean): this;
|
||||
setZoomBrush(zoomBrush?: Box2dModel | null): this;
|
||||
get shapeIds(): Set<TLShapeId>;
|
||||
get shapesArray(): TLShape[];
|
||||
|
@ -507,6 +524,7 @@ export class App extends EventEmitter {
|
|||
get zoomLevel(): number;
|
||||
zoomOut(point?: Vec2d, opts?: AnimationOptions): this;
|
||||
zoomToBounds(x: number, y: number, width: number, height: number, targetZoom?: number, opts?: AnimationOptions): this;
|
||||
zoomToContent(): this;
|
||||
zoomToFit(opts?: AnimationOptions): this;
|
||||
zoomToSelection(opts?: AnimationOptions): this;
|
||||
}
|
||||
|
@ -1729,7 +1747,7 @@ export type TLCancelEventInfo = {
|
|||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLChange = HistoryEntry<any>;
|
||||
export type TLChange<T extends BaseRecord<any> = any> = HistoryEntry<T>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLClickEvent = (info: TLClickEventInfo) => void;
|
||||
|
@ -1990,6 +2008,48 @@ export interface TLEventHandlers {
|
|||
// @public (undocumented)
|
||||
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)
|
||||
export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName;
|
||||
|
||||
|
@ -2403,9 +2463,6 @@ export type TLPointerEventTarget = {
|
|||
shape: TLShape;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLReorderOperation = 'backward' | 'forward' | 'toBack' | 'toFront';
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLResizeHandle = SelectionCorner | SelectionEdge;
|
||||
|
||||
|
|
|
@ -1392,6 +1392,7 @@ input,
|
|||
.tl-error-boundary__overlay {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
z-index: 500;
|
||||
background-color: var(--color-overlay);
|
||||
}
|
||||
|
||||
|
@ -1435,6 +1436,7 @@ it from receiving any pointer events or affecting the cursor. */
|
|||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
overflow: auto;
|
||||
z-index: 600;
|
||||
}
|
||||
.tl-error-boundary__content__expanded {
|
||||
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 { TLBoxTool, type TLBoxLike } from './lib/app/statechart/TLBoxTool/TLBoxTool'
|
||||
export { type ClipboardPayload, type TLClipboardModel } from './lib/app/types/clipboard-types'
|
||||
export { type TLEventMap, type TLEventMapHandler } from './lib/app/types/emit-types'
|
||||
export {
|
||||
EVENT_NAME_MAP,
|
||||
type TLBaseEventInfo,
|
||||
|
@ -106,7 +107,6 @@ export {
|
|||
type TLMark,
|
||||
} from './lib/app/types/history-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 {
|
||||
defaultEditorAssetUrls,
|
||||
|
|
|
@ -237,7 +237,11 @@ function TldrawEditorAfterLoading({
|
|||
}
|
||||
}, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl])
|
||||
|
||||
const onMountEvent = useEvent((app: App) => onMount?.(app))
|
||||
const onMountEvent = useEvent((app: App) => {
|
||||
onMount?.(app)
|
||||
app.emit('mount')
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (app) {
|
||||
// Set the initial theme state.
|
||||
|
|
|
@ -55,7 +55,7 @@ import {
|
|||
TLVideoAsset,
|
||||
Vec2dModel,
|
||||
} 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 { EventEmitter } from 'eventemitter3'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
@ -126,13 +126,13 @@ import { TLTextShapeDef } from './shapeutils/TLTextUtil/TLTextUtil'
|
|||
import { RootState } from './statechart/RootState'
|
||||
import { StateNode } from './statechart/StateNode'
|
||||
import { TLClipboardModel } from './types/clipboard-types'
|
||||
import { TLEventMap } from './types/emit-types'
|
||||
import { TLEventInfo, TLPinchEventInfo, TLPointerEventInfo } from './types/event-types'
|
||||
import { RequiredKeys } from './types/misc-types'
|
||||
import { TLReorderOperation } from './types/reorder-types'
|
||||
import { TLResizeHandle } from './types/selection-types'
|
||||
|
||||
/** @public */
|
||||
export type TLChange = HistoryEntry<any>
|
||||
export type TLChange<T extends BaseRecord<any> = any> = HistoryEntry<T>
|
||||
|
||||
/** @public */
|
||||
export type AnimationOptions = Partial<{
|
||||
|
@ -168,7 +168,7 @@ export function isShapeWithHandles(shape: TLShape) {
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export class App extends EventEmitter {
|
||||
export class App extends EventEmitter<TLEventMap> {
|
||||
constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) {
|
||||
super()
|
||||
|
||||
|
@ -282,14 +282,14 @@ export class App extends EventEmitter {
|
|||
true
|
||||
)
|
||||
|
||||
this.root.enter(undefined, 'initial')
|
||||
|
||||
if (this.instanceState.followingUserId) {
|
||||
this.stopFollowingUser()
|
||||
}
|
||||
|
||||
this.updateCullingBounds()
|
||||
|
||||
this.root.enter(undefined, 'initial')
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._tickManager.start()
|
||||
})
|
||||
|
@ -533,27 +533,71 @@ export class App extends EventEmitter {
|
|||
crash(error: unknown) {
|
||||
this._crashingError = error
|
||||
this.store.markAsPossiblyCorrupted()
|
||||
this.emit('crash')
|
||||
this.emit('crash', { error })
|
||||
}
|
||||
|
||||
get devicePixelRatio() {
|
||||
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
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
get isMenuOpen() {
|
||||
return this.openMenus.size > 0
|
||||
@computed get isMenuOpen() {
|
||||
return this.openMenus.length > 0
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -1452,6 +1496,50 @@ export class App extends EventEmitter {
|
|||
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 */
|
||||
@computed private get _userDocumentSettings() {
|
||||
return this.store.query.record('user_document', () => ({ userId: { eq: this.userId } }))
|
||||
|
@ -1461,27 +1549,29 @@ export class App extends EventEmitter {
|
|||
return this._userDocumentSettings.value!
|
||||
}
|
||||
|
||||
get isReadOnly() {
|
||||
return this.userDocumentSettings.isReadOnly
|
||||
}
|
||||
|
||||
get isGridMode() {
|
||||
return this.userDocumentSettings.isGridMode
|
||||
}
|
||||
|
||||
setGridMode(isGridMode: boolean) {
|
||||
this.updateUserDocumentSettings({ isGridMode }, true)
|
||||
}
|
||||
|
||||
setDarkMode(isDarkMode: boolean) {
|
||||
this.updateUserDocumentSettings({ isDarkMode }, true)
|
||||
}
|
||||
|
||||
setReadOnly(isReadOnly: boolean) {
|
||||
this.updateUserDocumentSettings({ isReadOnly }, true)
|
||||
if (isReadOnly) {
|
||||
this.setSelectedTool('hand')
|
||||
setGridMode(isGridMode: boolean): this {
|
||||
if (isGridMode === this.isGridMode) {
|
||||
this.updateUserDocumentSettings({ isGridMode }, true)
|
||||
}
|
||||
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 */
|
||||
|
@ -1494,10 +1584,12 @@ export class App extends EventEmitter {
|
|||
return this._isPenMode.value
|
||||
}
|
||||
|
||||
setPenMode(isPenMode: boolean) {
|
||||
setPenMode(isPenMode: boolean): this {
|
||||
if (isPenMode) this._touchEventsRemainingBeforeExitingPenMode = 3
|
||||
|
||||
this._isPenMode.set(isPenMode)
|
||||
if (isPenMode !== this.isPenMode) {
|
||||
this._isPenMode.set(isPenMode)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// User / User App State
|
||||
|
@ -4410,6 +4502,8 @@ export class App extends EventEmitter {
|
|||
|
||||
return {
|
||||
data: {
|
||||
currentPageId: this.currentPageId,
|
||||
createdIds: partials.map((p) => p.id),
|
||||
prevSelectedIds,
|
||||
partials: partialsToCreate,
|
||||
select,
|
||||
|
@ -4417,7 +4511,7 @@ export class App extends EventEmitter {
|
|||
}
|
||||
},
|
||||
{
|
||||
do: ({ partials, select }) => {
|
||||
do: ({ createdIds, partials, select }) => {
|
||||
const { focusLayerId } = this
|
||||
|
||||
// 1. Parents
|
||||
|
@ -4463,7 +4557,7 @@ export class App extends EventEmitter {
|
|||
|
||||
const parentIndices = new Map<string, string>()
|
||||
|
||||
const shapeRecordsTocreate: TLShape[] = []
|
||||
const shapeRecordsToCreate: TLShape[] = []
|
||||
|
||||
for (const partial of partials) {
|
||||
const util = this.getShapeUtil(partial as TLShape)
|
||||
|
@ -4516,20 +4610,23 @@ export class App extends EventEmitter {
|
|||
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;
|
||||
|
||||
// the engine will filter out any shapes that are descendants of other new shapes.
|
||||
if (select) {
|
||||
const selectedIds = partials.map((partial) => partial.id)
|
||||
this.store.update(this.pageState.id, (state) => ({ ...state, selectedIds }))
|
||||
this.store.update(this.pageState.id, (state) => ({
|
||||
...state,
|
||||
selectedIds: createdIds,
|
||||
}))
|
||||
}
|
||||
},
|
||||
undo: ({ partials, prevSelectedIds }) => {
|
||||
this.store.remove(partials.map((p) => p.id))
|
||||
undo: ({ createdIds, prevSelectedIds }) => {
|
||||
this.store.remove(createdIds)
|
||||
|
||||
if (prevSelectedIds) {
|
||||
this.store.update(this.pageState.id, (state) => ({
|
||||
|
@ -4987,6 +5084,7 @@ export class App extends EventEmitter {
|
|||
undo: ({ newPage, prevPageState, prevTabState, newTabPageState }) => {
|
||||
this.store.put([prevPageState, prevTabState])
|
||||
this.store.remove([newTabPageState.id, newPage.id, newTabPageState.cameraId])
|
||||
|
||||
this.updateCullingBounds()
|
||||
},
|
||||
}
|
||||
|
@ -5271,6 +5369,7 @@ export class App extends EventEmitter {
|
|||
this.store.put(assets)
|
||||
},
|
||||
undo: ({ assets }) => {
|
||||
// todo: should we actually remove assets here? or on cleanup elsewhere?
|
||||
this.store.remove(assets.map((a) => a.id))
|
||||
},
|
||||
}
|
||||
|
@ -5739,8 +5838,6 @@ export class App extends EventEmitter {
|
|||
this.centerOnPoint(x, y)
|
||||
})
|
||||
|
||||
this.emit('moved-to-page', { name: this.currentPage.name, toId: pageId, fromId: currentPageId })
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -5757,9 +5854,10 @@ export class App extends EventEmitter {
|
|||
* @param ids - The ids to reorder.
|
||||
* @public
|
||||
*/
|
||||
reorderShapes(operation: TLReorderOperation, ids: TLShapeId[]) {
|
||||
reorderShapes(operation: 'toBack' | 'toFront' | 'forward' | 'backward', ids: TLShapeId[]) {
|
||||
if (this.isReadOnly) return this
|
||||
if (ids.length === 0) return this
|
||||
// this.emit('reorder-shapes', { pageId: this.currentPageId, ids, operation })
|
||||
|
||||
const parents = this.getParentsMappedToChildren(ids)
|
||||
|
||||
|
@ -6980,6 +7078,7 @@ export class App extends EventEmitter {
|
|||
// page might have no shapes
|
||||
if (ids.length <= 0) return this
|
||||
this.setSelectedIds(ids)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -7020,10 +7119,11 @@ export class App extends EventEmitter {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
selectNone() {
|
||||
selectNone(): this {
|
||||
if (this.selectedIds.length > 0) {
|
||||
this.setSelectedIds([])
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -7040,7 +7140,7 @@ export class App extends EventEmitter {
|
|||
* @param options - Options for setting the current page.
|
||||
* @public
|
||||
*/
|
||||
setCurrentPageId(pageId: TLPageId, { stopFollowing = true }: ViewportOptions = {}) {
|
||||
setCurrentPageId(pageId: TLPageId, { stopFollowing = true }: ViewportOptions = {}): this {
|
||||
this._setCurrentPageId(pageId, { stopFollowing })
|
||||
return this
|
||||
}
|
||||
|
@ -7059,42 +7159,42 @@ export class App extends EventEmitter {
|
|||
}
|
||||
|
||||
return {
|
||||
data: { pageId, prev: this.currentPageId },
|
||||
data: { toId: pageId, fromId: this.currentPageId },
|
||||
squashing: true,
|
||||
preservesRedoStack: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
do: ({ pageId }) => {
|
||||
if (!this.getPageStateByPageId(pageId)) {
|
||||
do: ({ toId }) => {
|
||||
if (!this.getPageStateByPageId(toId)) {
|
||||
const camera = TLCamera.create({})
|
||||
this.store.put([
|
||||
camera,
|
||||
TLInstancePageState.create({
|
||||
pageId,
|
||||
pageId: toId,
|
||||
instanceId: this.instanceId,
|
||||
cameraId: camera.id,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
this.store.put([{ ...this.instanceState, currentPageId: pageId }])
|
||||
this.store.put([{ ...this.instanceState, currentPageId: toId }])
|
||||
|
||||
this.updateUserPresence({
|
||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
||||
})
|
||||
this.updateCullingBounds()
|
||||
},
|
||||
undo: ({ prev }) => {
|
||||
this.store.put([{ ...this.instanceState, currentPageId: prev }])
|
||||
undo: ({ fromId }) => {
|
||||
this.store.put([{ ...this.instanceState, currentPageId: fromId }])
|
||||
|
||||
this.updateUserPresence({
|
||||
viewportPageBounds: this.viewportPageBounds.toJson(),
|
||||
})
|
||||
this.updateCullingBounds()
|
||||
},
|
||||
squash: ({ prev }, { pageId }) => {
|
||||
return { pageId, prev }
|
||||
squash: ({ fromId }, { toId }) => {
|
||||
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
|
||||
* @public
|
||||
*/
|
||||
setHoveredId(id: TLShapeId | null = null) {
|
||||
setHoveredId(id: TLShapeId | null = null): this {
|
||||
if (id === this.pageState.hoveredId) return this
|
||||
|
||||
this.setInstancePageState({ hoveredId: id }, true)
|
||||
|
@ -7181,7 +7281,7 @@ export class App extends EventEmitter {
|
|||
* @param ids - The ids of shapes to set as erasing.
|
||||
* @public
|
||||
*/
|
||||
setErasingIds(ids: TLShapeId[] = []) {
|
||||
setErasingIds(ids: TLShapeId[] = []): this {
|
||||
const erasingIds = this.erasingIdsSet
|
||||
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.
|
||||
* @public
|
||||
*/
|
||||
setCursor(cursor: Partial<TLCursor>) {
|
||||
setCursor(cursor: Partial<TLCursor>): this {
|
||||
const current = this.cursor
|
||||
const next = {
|
||||
...current,
|
||||
|
@ -7236,7 +7336,7 @@ export class App extends EventEmitter {
|
|||
* @param scribble - The new scribble object.
|
||||
* @public
|
||||
*/
|
||||
setScribble(scribble: TLScribble | null = null) {
|
||||
setScribble(scribble: TLScribble | null = null): this {
|
||||
this.updateInstanceState({ scribble }, true)
|
||||
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.
|
||||
* @public
|
||||
*/
|
||||
setBrush(brush: Box2dModel | null = null) {
|
||||
setBrush(brush: Box2dModel | null = null): this {
|
||||
if (!brush && !this.brush) return this
|
||||
this.updateInstanceState({ brush }, true)
|
||||
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.
|
||||
* @public
|
||||
*/
|
||||
setZoomBrush(zoomBrush: Box2dModel | null = null) {
|
||||
setZoomBrush(zoomBrush: Box2dModel | null = null): this {
|
||||
if (!zoomBrush && !this.zoomBrush) return this
|
||||
this.updateInstanceState({ zoomBrush }, true)
|
||||
return this
|
||||
|
@ -7297,6 +7397,7 @@ export class App extends EventEmitter {
|
|||
|
||||
const snapshot = getRotationSnapshot({ app: this })
|
||||
applyRotationToSnapshotShapes({ delta, snapshot, app: this, stage: 'one-off' })
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -7703,31 +7804,33 @@ export class App extends EventEmitter {
|
|||
private _setCamera(x: number, y: number, z = this.camera.z) {
|
||||
const currentCamera = this.camera
|
||||
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({
|
||||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_move',
|
||||
point: currentScreenPoint,
|
||||
pointerId: 0,
|
||||
ctrlKey: this.inputs.ctrlKey,
|
||||
altKey: this.inputs.altKey,
|
||||
shiftKey: this.inputs.shiftKey,
|
||||
button: 0,
|
||||
isPen: this.isPenMode ?? false,
|
||||
const { currentScreenPoint } = this.inputs
|
||||
|
||||
this.dispatch({
|
||||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_move',
|
||||
point: currentScreenPoint,
|
||||
pointerId: 0,
|
||||
ctrlKey: this.inputs.ctrlKey,
|
||||
altKey: this.inputs.altKey,
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -7829,6 +7932,28 @@ export class App extends EventEmitter {
|
|||
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.
|
||||
*
|
||||
|
@ -7880,6 +8005,7 @@ export class App extends EventEmitter {
|
|||
} else {
|
||||
this.setCamera(cx + (x / 1 - x) - (x / cz - x), cy + (y / 1 - y) - (y / cz - y), 1)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -7923,6 +8049,7 @@ export class App extends EventEmitter {
|
|||
} else {
|
||||
this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -7967,6 +8094,7 @@ export class App extends EventEmitter {
|
|||
} else {
|
||||
this.setCamera(cx + (x / zoom - x) - (x / cz - x), cy + (y / zoom - y) - (y / cz - y), zoom)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -7998,6 +8126,7 @@ export class App extends EventEmitter {
|
|||
Math.max(1, this.camera.z),
|
||||
opts
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -8711,6 +8840,60 @@ export class App extends EventEmitter {
|
|||
|
||||
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) {
|
||||
|
|
|
@ -18,7 +18,11 @@ type CommandFn<Data> = (...args: any[]) =>
|
|||
type ExtractData<Fn> = Fn extends CommandFn<infer Data> ? Data : never
|
||||
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
|
||||
_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
|
||||
|
@ -103,7 +107,7 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
|
|||
this._redos.set(stack())
|
||||
}
|
||||
|
||||
this.ctx.emit('change-history')
|
||||
this.ctx.emit('change-history', { reason: 'push' })
|
||||
}
|
||||
|
||||
return this.ctx
|
||||
|
@ -165,7 +169,6 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
|
|||
}) => {
|
||||
this.ignoringUpdates((undos, redos) => {
|
||||
if (undos.length === 0) {
|
||||
this.ctx.emit('change-history')
|
||||
return { undos, redos }
|
||||
}
|
||||
|
||||
|
@ -176,13 +179,19 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
|
|||
redos = redos.push(mark)
|
||||
}
|
||||
if (mark.id === toMark) {
|
||||
this.ctx.emit('change-history')
|
||||
this.ctx.emit(
|
||||
'change-history',
|
||||
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
|
||||
)
|
||||
return { undos, redos }
|
||||
}
|
||||
}
|
||||
|
||||
if (undos.length === 0) {
|
||||
this.ctx.emit('change-history')
|
||||
this.ctx.emit(
|
||||
'change-history',
|
||||
pushToRedoStack ? { reason: 'undo' } : { reason: 'bail', markId: toMark }
|
||||
)
|
||||
return { undos, redos }
|
||||
}
|
||||
|
||||
|
@ -196,7 +205,10 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
|
|||
|
||||
if (command.type === 'STOP') {
|
||||
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 }
|
||||
}
|
||||
} 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 }
|
||||
})
|
||||
|
||||
|
@ -221,7 +236,6 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
|
|||
redo = () => {
|
||||
this.ignoringUpdates((undos, redos) => {
|
||||
if (redos.length === 0) {
|
||||
this.ctx.emit('change-history')
|
||||
return { undos, redos }
|
||||
}
|
||||
|
||||
|
@ -231,7 +245,7 @@ export class HistoryManager<CTX extends { emit: (name: string) => void }> {
|
|||
}
|
||||
|
||||
if (redos.length === 0) {
|
||||
this.ctx.emit('change-history')
|
||||
this.ctx.emit('change-history', { reason: 'redo' })
|
||||
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 }
|
||||
})
|
||||
|
||||
|
@ -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.ctx.emit('mark-history', { 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', () => {
|
||||
it('does nothing', () => {
|
||||
const fn = jest.fn()
|
||||
app.on('update_node', fn)
|
||||
app.setSelectedIds([ids.boxA, ids.boxB])
|
||||
const fn = jest.fn()
|
||||
app.on('change-history', fn)
|
||||
app.distributeShapes('horizontal')
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
|
|
|
@ -50,9 +50,9 @@ describe('distributeShapes command', () => {
|
|||
|
||||
describe('when less than three shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
const fn = jest.fn()
|
||||
app.on('update_node', fn)
|
||||
app.setSelectedIds([ids.boxA, ids.boxB])
|
||||
const fn = jest.fn()
|
||||
app.on('change-history', fn)
|
||||
app.stackShapes('horizontal')
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
|
|
|
@ -69,9 +69,9 @@ beforeEach(() => {
|
|||
|
||||
describe('when less than two shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
const fn = jest.fn()
|
||||
app.on('update_shape', fn)
|
||||
app.setSelectedIds([ids.boxB])
|
||||
const fn = jest.fn()
|
||||
app.on('change-history', fn)
|
||||
app.stretchShapes('horizontal')
|
||||
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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
const shapesBefore = app.shapesArray.length
|
||||
app
|
||||
.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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
expect(app.shapesArray.length).toBe(0)
|
||||
app.setSelectedTool('frame')
|
||||
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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app.setSelectedTool('frame')
|
||||
app.pointerDown(50, 50)
|
||||
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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
expect(app.shapesArray.length).toBe(0)
|
||||
app.setSelectedTool('geo')
|
||||
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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app.setSelectedTool('geo')
|
||||
app.pointerDown(50, 50)
|
||||
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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
const shapesBefore = app.shapesArray.length
|
||||
app
|
||||
.setSelectedTool('line')
|
||||
|
@ -113,7 +113,7 @@ describe('When dragging the line', () => {
|
|||
|
||||
describe('When extending the line with the shift-key in tool-lock mode', () => {
|
||||
it('extends a line by joining-the-dots', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app
|
||||
.setSelectedTool('line')
|
||||
.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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app
|
||||
.setSelectedTool('line')
|
||||
.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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app
|
||||
.setSelectedTool('line')
|
||||
.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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app
|
||||
.setSelectedTool('line')
|
||||
.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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app
|
||||
.setSelectedTool('line')
|
||||
.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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
app.setSelectedTool('note')
|
||||
app.pointerDown(50, 50)
|
||||
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', () => {
|
||||
app.updateInstanceState({ isToolLocked: true })
|
||||
app.setToolLocked(true)
|
||||
expect(app.shapesArray.length).toBe(0)
|
||||
app.setSelectedTool('note')
|
||||
app.pointerDown(50, 50)
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface ActionItem {
|
|||
// (undocumented)
|
||||
menuLabel?: TLTranslationKey;
|
||||
// (undocumented)
|
||||
onSelect: () => Promise<void> | void;
|
||||
onSelect: (source: TLUiEventSource) => Promise<void> | void;
|
||||
// (undocumented)
|
||||
readonlyOk: boolean;
|
||||
// (undocumented)
|
||||
|
@ -636,7 +636,7 @@ export const TldrawUi: React_2.NamedExoticComponent<{
|
|||
export const TldrawUiContent: React_2.NamedExoticComponent<TldrawUiContentProps>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function TldrawUiContextProvider({ overrides, assetUrls, children, }: TldrawUiContextProviderProps): JSX.Element;
|
||||
export function TldrawUiContextProvider({ overrides, assetUrls, onEvent, children, }: TldrawUiContextProviderProps): JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TldrawUiContextProviderProps {
|
||||
|
@ -645,6 +645,8 @@ export interface TldrawUiContextProviderProps {
|
|||
// (undocumented)
|
||||
children?: any;
|
||||
// (undocumented)
|
||||
onEvent?: TLUiEventHandler;
|
||||
// (undocumented)
|
||||
overrides?: TldrawUiOverrides | TldrawUiOverrides[];
|
||||
}
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ export const TldrawUiContent = React.memo(function TldrawUI({
|
|||
className="tlui-focus-button"
|
||||
title={`${msg('focus-mode.toggle-focus-mode')}`}
|
||||
icon="dot"
|
||||
onClick={toggleFocus.onSelect}
|
||||
onClick={() => toggleFocus.onSelect('menu')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AssetUrlsProvider } from './hooks/useAssetUrls'
|
|||
import { BreakPointProvider } from './hooks/useBreakpoint'
|
||||
import { ContextMenuSchemaProvider } from './hooks/useContextMenuSchema'
|
||||
import { DialogsProvider } from './hooks/useDialogsProvider'
|
||||
import { EventsProvider, TLUiEventHandler } from './hooks/useEventsProvider'
|
||||
import { HelpMenuSchemaProvider } from './hooks/useHelpMenuSchema'
|
||||
import { KeyboardShortcutsSchemaProvider } from './hooks/useKeyboardShortcutsSchema'
|
||||
import { MenuSchemaProvider } from './hooks/useMenuSchema'
|
||||
|
@ -18,6 +19,7 @@ import { TldrawUiOverrides, useMergedOverrides, useMergedTranslationOverrides }
|
|||
export interface TldrawUiContextProviderProps {
|
||||
assetUrls?: UiAssetUrls
|
||||
overrides?: TldrawUiOverrides | TldrawUiOverrides[]
|
||||
onEvent?: TLUiEventHandler
|
||||
children?: any
|
||||
}
|
||||
|
||||
|
@ -25,6 +27,7 @@ export interface TldrawUiContextProviderProps {
|
|||
export function TldrawUiContextProvider({
|
||||
overrides,
|
||||
assetUrls,
|
||||
onEvent,
|
||||
children,
|
||||
}: TldrawUiContextProviderProps) {
|
||||
return (
|
||||
|
@ -33,7 +36,9 @@ export function TldrawUiContextProvider({
|
|||
<ToastsProvider>
|
||||
<DialogsProvider>
|
||||
<BreakPointProvider>
|
||||
<InternalProviders overrides={overrides}>{children}</InternalProviders>
|
||||
<EventsProvider onEvent={onEvent}>
|
||||
<InternalProviders overrides={overrides}>{children}</InternalProviders>
|
||||
</EventsProvider>
|
||||
</BreakPointProvider>
|
||||
</DialogsProvider>
|
||||
</ToastsProvider>
|
||||
|
|
|
@ -37,7 +37,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
|
|||
? `${kbdStr(kbd)}`
|
||||
: ''
|
||||
}
|
||||
onClick={onSelect}
|
||||
onClick={() => onSelect('actions-menu')}
|
||||
disabled={item.disabled}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -42,7 +42,7 @@ export function BackToContent() {
|
|||
iconLeft={action.icon}
|
||||
label={action.label}
|
||||
onClick={() => {
|
||||
action.onSelect()
|
||||
action.onSelect('helper-buttons')
|
||||
setShowBackToContent(false)
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -164,7 +164,7 @@ function ContextMenuContent() {
|
|||
dir="ltr"
|
||||
disabled={item.disabled}
|
||||
onSelect={(e) => {
|
||||
onSelect()
|
||||
onSelect('context-menu')
|
||||
preventDefault(e)
|
||||
}}
|
||||
title={labelStr ? labelStr : undefined}
|
||||
|
@ -199,7 +199,7 @@ function ContextMenuContent() {
|
|||
if (disableClicks) {
|
||||
setDisableClicks(false)
|
||||
} else {
|
||||
onSelect()
|
||||
onSelect('context-menu')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -16,7 +16,7 @@ export const DuplicateButton = track(function DuplicateButton() {
|
|||
return (
|
||||
<Button
|
||||
icon={action.icon}
|
||||
onClick={action.onSelect}
|
||||
onClick={() => action.onSelect('quick-actions')}
|
||||
disabled={noSelected}
|
||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||
smallIcon
|
||||
|
|
|
@ -25,7 +25,7 @@ export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps)
|
|||
|
||||
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)
|
||||
|
||||
|
@ -46,7 +46,7 @@ export const EditLinkDialog = track(function EditLink({ onClose }: DialogProps)
|
|||
setUrlValue(value)
|
||||
|
||||
const validStateUrl = valiateUrl(value.trim())
|
||||
setIsValid((s) => (s === validStateUrl ? s : validStateUrl))
|
||||
setValid((s) => (s === validStateUrl ? s : validStateUrl))
|
||||
if (validStateUrl) {
|
||||
rValue.current = value
|
||||
}
|
||||
|
|
|
@ -88,7 +88,15 @@ function HelpMenuContent() {
|
|||
}
|
||||
case 'item': {
|
||||
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 (
|
||||
<M.CheckboxItem
|
||||
key={id}
|
||||
onSelect={onSelect}
|
||||
onSelect={() => onSelect('menu')}
|
||||
title={labelStr ? labelStr : ''}
|
||||
checked={item.checked}
|
||||
disabled={item.disabled}
|
||||
|
@ -139,7 +139,7 @@ function MenuContent() {
|
|||
data-wd={`menu-item.${item.id}`}
|
||||
kbd={kbd}
|
||||
label={labelToUse}
|
||||
onClick={onSelect}
|
||||
onClick={() => onSelect('menu')}
|
||||
disabled={item.disabled}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as _ContextMenu from '@radix-ui/react-context-menu'
|
||||
import { TLPage, TLPageId, useApp, useContainer } from '@tldraw/editor'
|
||||
import { track } from 'signia-react'
|
||||
import { useToasts } from '../hooks/useToastsProvider'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
|
||||
|
@ -10,6 +11,7 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
|
|||
const pages = app.pages
|
||||
const currentPageId = app.currentPageId
|
||||
const msg = useTranslation()
|
||||
const { addToast } = useToasts()
|
||||
|
||||
return (
|
||||
<_ContextMenu.Sub>
|
||||
|
@ -36,6 +38,25 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
|
|||
onSelect={() => {
|
||||
app.mark('move_shapes_to_page')
|
||||
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
|
||||
>
|
||||
|
|
|
@ -46,14 +46,14 @@ export const NavigationZone = memo(function NavigationZone() {
|
|||
icon="minus"
|
||||
data-wd="minimap.zoom-out"
|
||||
title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`}
|
||||
onClick={actions['zoom-out'].onSelect}
|
||||
onClick={() => actions['zoom-out'].onSelect('navigation-zone')}
|
||||
/>
|
||||
<ZoomMenu />
|
||||
<Button
|
||||
icon="plus"
|
||||
data-wd="minimap.zoom-in"
|
||||
title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`}
|
||||
onClick={actions['zoom-in'].onSelect}
|
||||
onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
|
||||
/>
|
||||
<Button
|
||||
title={msg('navigation-zone.toggle-minimap')}
|
||||
|
|
|
@ -78,7 +78,7 @@ function ZoomMenuItem(props: {
|
|||
label={actions[action].label}
|
||||
kbd={actions[action].kbd}
|
||||
data-wd={props['data-wd']}
|
||||
onClick={actions[action].onSelect}
|
||||
onClick={() => actions[action].onSelect('zoom-menu')}
|
||||
noClose={noClose}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
@ -14,5 +14,11 @@ export const ExitPenMode = track(function ExitPenMode() {
|
|||
|
||||
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}
|
||||
title={`${msg(redo.label!)} ${kbdStr(redo.kbd!)}`}
|
||||
disabled={!canRedo}
|
||||
onClick={redo.onSelect}
|
||||
onClick={() => redo.onSelect('quick-actions')}
|
||||
smallIcon
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -13,5 +13,11 @@ export const StopFollowing = track(function ExitPenMode() {
|
|||
|
||||
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[]
|
||||
}) {
|
||||
const msg = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="tlui-button-grid__four tlui-button-grid__reverse">
|
||||
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => {
|
||||
|
|
|
@ -13,6 +13,7 @@ export const TrashButton = track(function TrashButton() {
|
|||
const action = actions['delete']
|
||||
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
if (isReadonly) return null
|
||||
|
||||
const noSelected = app.selectedIds.length <= 0
|
||||
|
@ -20,7 +21,7 @@ export const TrashButton = track(function TrashButton() {
|
|||
return (
|
||||
<Button
|
||||
icon={action.icon}
|
||||
onClick={action.onSelect}
|
||||
onClick={() => action.onSelect('quick-actions')}
|
||||
disabled={noSelected}
|
||||
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}
|
||||
smallIcon
|
||||
|
|
|
@ -18,7 +18,7 @@ export const UndoButton = memo(function UndoButton() {
|
|||
icon={undo.icon}
|
||||
title={`${msg(undo.label!)} ${kbdStr(undo.kbd!)}`}
|
||||
disabled={!canUndo}
|
||||
onClick={undo.onSelect}
|
||||
onClick={() => undo.onSelect('quick-actions')}
|
||||
smallIcon
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -21,6 +21,7 @@ import { TLUiIconType } from '../icon-types'
|
|||
import { useMenuClipboardEvents } from './useClipboardEvents'
|
||||
import { useCopyAs } from './useCopyAs'
|
||||
import { useDialogs } from './useDialogsProvider'
|
||||
import { TLUiEventSource, useEvents } from './useEventsProvider'
|
||||
import { useExportAs } from './useExportAs'
|
||||
import { useInsertMedia } from './useInsertMedia'
|
||||
import { usePrint } from './usePrint'
|
||||
|
@ -39,7 +40,7 @@ export interface ActionItem {
|
|||
contextMenuLabel?: TLTranslationKey
|
||||
readonlyOk: boolean
|
||||
checkbox?: boolean
|
||||
onSelect: () => Promise<void> | void
|
||||
onSelect: (source: TLUiEventSource) => Promise<void> | void
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -76,46 +77,18 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
const copyAs = useCopyAs()
|
||||
const exportAs = useExportAs()
|
||||
|
||||
const trackEvent = useEvents()
|
||||
|
||||
// should this be a useMemo? looks like it doesn't actually deref any reactive values
|
||||
const actions = React.useMemo<ActionsContextType>(() => {
|
||||
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',
|
||||
label: 'action.edit-link',
|
||||
icon: 'link',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'edit-link')
|
||||
app.mark('edit-link')
|
||||
addDialog({ component: EditLinkDialog })
|
||||
},
|
||||
|
@ -125,7 +98,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.insert-embed',
|
||||
readonlyOk: false,
|
||||
kbd: '$i',
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'insert-embed')
|
||||
addDialog({ component: EmbedDialog })
|
||||
},
|
||||
},
|
||||
|
@ -134,7 +108,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.insert-media',
|
||||
kbd: '$u',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'insert-media')
|
||||
insertMedia()
|
||||
},
|
||||
},
|
||||
|
@ -144,7 +119,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'undo',
|
||||
kbd: '$z',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'undo')
|
||||
app.undo()
|
||||
},
|
||||
},
|
||||
|
@ -154,7 +130,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'redo',
|
||||
kbd: '$!z',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'redo')
|
||||
app.redo()
|
||||
},
|
||||
},
|
||||
|
@ -164,7 +141,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.export-as-svg.short',
|
||||
contextMenuLabel: 'action.export-as-svg.short',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'export-as', { format: 'svg' })
|
||||
exportAs(app.selectedIds, 'svg')
|
||||
},
|
||||
},
|
||||
|
@ -174,7 +152,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.export-as-png.short',
|
||||
contextMenuLabel: 'action.export-as-png.short',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'export-as', { format: 'png' })
|
||||
exportAs(app.selectedIds, 'png')
|
||||
},
|
||||
},
|
||||
|
@ -184,7 +163,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.export-as-json.short',
|
||||
contextMenuLabel: 'action.export-as-json.short',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'export-as', { format: 'json' })
|
||||
exportAs(app.selectedIds, 'json')
|
||||
},
|
||||
},
|
||||
|
@ -195,7 +175,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.copy-as-svg.short',
|
||||
kbd: '$!c',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'copy-as', { format: 'svg' })
|
||||
copyAs(app.selectedIds, 'svg')
|
||||
},
|
||||
},
|
||||
|
@ -205,7 +186,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.copy-as-png.short',
|
||||
contextMenuLabel: 'action.copy-as-png.short',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'copy-as', { format: 'png' })
|
||||
copyAs(app.selectedIds, 'png')
|
||||
},
|
||||
},
|
||||
|
@ -215,7 +197,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.copy-as-json.short',
|
||||
contextMenuLabel: 'action.copy-as-json.short',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'copy-as', { format: 'json' })
|
||||
copyAs(app.selectedIds, 'json')
|
||||
},
|
||||
},
|
||||
|
@ -223,7 +206,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
id: 'toggle-auto-size',
|
||||
label: 'action.toggle-auto-size',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-auto-size')
|
||||
app.mark()
|
||||
app.updateShapes(
|
||||
app.selectedShapes
|
||||
|
@ -246,7 +230,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
id: 'open-embed-link',
|
||||
label: 'action.open-embed-link',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'open-embed-link')
|
||||
const ids = app.selectedIds
|
||||
const warnMsg = 'No embed shapes selected'
|
||||
if (ids.length !== 1) {
|
||||
|
@ -266,7 +251,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
id: 'convert-to-bookmark',
|
||||
label: 'action.convert-to-bookmark',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'convert-to-bookmark')
|
||||
const ids = app.selectedIds
|
||||
const shapes = ids.map((id) => app.getShapeById(id))
|
||||
|
||||
|
@ -308,7 +294,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
id: 'convert-to-embed',
|
||||
label: 'action.convert-to-embed',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'convert-to-embed')
|
||||
const ids = app.selectedIds
|
||||
const shapes = compact(ids.map((id) => app.getShapeById(id)))
|
||||
|
||||
|
@ -358,8 +345,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.duplicate',
|
||||
icon: 'duplicate',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
if (app.currentToolId !== 'select') return
|
||||
trackEvent(source, 'duplicate-shapes')
|
||||
const ids = app.selectedIds
|
||||
const commonBounds = Box2d.Common(compact(ids.map((id) => app.getPageBoundsById(id))))
|
||||
const offset = app.canMoveCamera
|
||||
|
@ -381,7 +369,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '$!g',
|
||||
icon: 'ungroup',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'ungroup-shapes')
|
||||
app.mark('ungroup')
|
||||
app.ungroupShapes(app.selectedIds)
|
||||
},
|
||||
|
@ -392,7 +381,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '$g',
|
||||
icon: 'group',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'group-shapes')
|
||||
if (app.selectedShapes.length === 1 && app.selectedShapes[0].type === 'group') {
|
||||
app.mark('ungroup')
|
||||
app.ungroupShapes(app.selectedIds)
|
||||
|
@ -408,7 +398,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?A',
|
||||
icon: 'align-left',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'align-shapes', { operation: 'left' })
|
||||
app.mark('align left')
|
||||
app.alignShapes('left', app.selectedIds)
|
||||
},
|
||||
|
@ -420,7 +411,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?H',
|
||||
icon: 'align-center-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'align-shapes', { operation: 'center-horizontal' })
|
||||
app.mark('align center horizontal')
|
||||
app.alignShapes('center-horizontal', app.selectedIds)
|
||||
},
|
||||
|
@ -431,7 +423,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?D',
|
||||
icon: 'align-right',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'align-shapes', { operation: 'right' })
|
||||
app.mark('align right')
|
||||
app.alignShapes('right', app.selectedIds)
|
||||
},
|
||||
|
@ -443,7 +436,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '?V',
|
||||
icon: 'align-center-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'align-shapes', { operation: 'center-vertical' })
|
||||
app.mark('align center vertical')
|
||||
app.alignShapes('center-vertical', app.selectedIds)
|
||||
},
|
||||
|
@ -454,7 +448,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'align-top',
|
||||
kbd: '?W',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'align-shapes', { operation: 'top' })
|
||||
app.mark('align top')
|
||||
app.alignShapes('top', app.selectedIds)
|
||||
},
|
||||
|
@ -465,7 +460,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'align-bottom',
|
||||
kbd: '?S',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'align-shapes', { operation: 'bottom' })
|
||||
app.mark('align bottom')
|
||||
app.alignShapes('bottom', app.selectedIds)
|
||||
},
|
||||
|
@ -476,7 +472,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.distribute-horizontal.short',
|
||||
icon: 'distribute-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'distribute-shapes', { operation: 'horizontal' })
|
||||
app.mark('distribute horizontal')
|
||||
app.distributeShapes('horizontal', app.selectedIds)
|
||||
},
|
||||
|
@ -487,7 +484,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.distribute-vertical.short',
|
||||
icon: 'distribute-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'distribute-shapes', { operation: 'vertical' })
|
||||
app.mark('distribute vertical')
|
||||
app.distributeShapes('vertical', app.selectedIds)
|
||||
},
|
||||
|
@ -498,7 +496,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.stretch-horizontal.short',
|
||||
icon: 'stretch-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'stretch-shapes', { operation: 'horizontal' })
|
||||
app.mark('stretch horizontal')
|
||||
app.stretchShapes('horizontal', app.selectedIds)
|
||||
},
|
||||
|
@ -509,7 +508,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.stretch-vertical.short',
|
||||
icon: 'stretch-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'stretch-shapes', { operation: 'vertical' })
|
||||
app.mark('stretch vertical')
|
||||
app.stretchShapes('vertical', app.selectedIds)
|
||||
},
|
||||
|
@ -520,7 +520,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.flip-horizontal.short',
|
||||
kbd: '!h',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'flip-shapes', { operation: 'horizontal' })
|
||||
app.mark('flip horizontal')
|
||||
app.flipShapes('horizontal', app.selectedIds)
|
||||
},
|
||||
|
@ -531,7 +532,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.flip-vertical.short',
|
||||
kbd: '!v',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'flip-shapes', { operation: 'vertical' })
|
||||
app.mark('flip vertical')
|
||||
app.flipShapes('vertical', app.selectedIds)
|
||||
},
|
||||
|
@ -541,7 +543,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.pack',
|
||||
icon: 'pack',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'pack-shapes')
|
||||
app.mark('pack')
|
||||
app.packShapes(app.selectedIds)
|
||||
},
|
||||
|
@ -552,7 +555,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.stack-vertical.short',
|
||||
icon: 'stack-vertical',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'stack-shapes', { operation: 'vertical' })
|
||||
app.mark('stack-vertical')
|
||||
app.stackShapes('vertical', app.selectedIds)
|
||||
},
|
||||
|
@ -563,7 +567,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
contextMenuLabel: 'action.stack-horizontal.short',
|
||||
icon: 'stack-horizontal',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'stack-shapes', { operation: 'horizontal' })
|
||||
app.mark('stack-horizontal')
|
||||
app.stackShapes('horizontal', app.selectedIds)
|
||||
},
|
||||
|
@ -574,7 +579,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: ']',
|
||||
icon: 'bring-to-front',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'reorder-shapes', { operation: 'toFront' })
|
||||
app.mark('bring to front')
|
||||
app.bringToFront()
|
||||
},
|
||||
|
@ -585,7 +591,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'bring-forward',
|
||||
kbd: '?]',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'reorder-shapes', { operation: 'forward' })
|
||||
app.mark('bring forward')
|
||||
app.bringForward()
|
||||
},
|
||||
|
@ -596,7 +603,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'send-backward',
|
||||
kbd: '?[',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'reorder-shapes', { operation: 'backward' })
|
||||
app.mark('send backward')
|
||||
app.sendBackward()
|
||||
},
|
||||
|
@ -607,7 +615,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'send-to-back',
|
||||
kbd: '[',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'reorder-shapes', { operation: 'toBack' })
|
||||
app.mark('send to back')
|
||||
app.sendToBack()
|
||||
},
|
||||
|
@ -617,7 +626,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.cut',
|
||||
kbd: '$x',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'cut')
|
||||
app.mark('cut')
|
||||
cut()
|
||||
},
|
||||
|
@ -627,7 +637,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.copy',
|
||||
kbd: '$c',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'copy')
|
||||
copy()
|
||||
},
|
||||
},
|
||||
|
@ -646,7 +657,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.select-all',
|
||||
kbd: '$a',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'select-all-shapes')
|
||||
if (app.currentToolId !== 'select') {
|
||||
app.cancel()
|
||||
app.setSelectedTool('select')
|
||||
|
@ -660,7 +672,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
id: 'select-none',
|
||||
label: 'action.select-none',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'select-none-shapes')
|
||||
app.mark('select none')
|
||||
app.selectNone()
|
||||
},
|
||||
|
@ -671,8 +684,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
kbd: '⌫',
|
||||
icon: 'trash',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
if (app.currentToolId !== 'select') return
|
||||
trackEvent(source, 'delete-shapes')
|
||||
app.mark('delete')
|
||||
app.deleteShapes()
|
||||
},
|
||||
|
@ -682,8 +696,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.rotate-cw',
|
||||
icon: 'rotate-cw',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
if (app.selectedIds.length === 0) return
|
||||
trackEvent(source, 'rotate-cw')
|
||||
app.mark('rotate-cw')
|
||||
const offset = app.selectionRotation % (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',
|
||||
icon: 'rotate-ccw',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
if (app.selectedIds.length === 0) return
|
||||
trackEvent(source, 'rotate-ccw')
|
||||
app.mark('rotate-ccw')
|
||||
const offset = app.selectionRotation % (TAU / 2)
|
||||
const offsetCloseToZero = approximately(offset, 0)
|
||||
|
@ -708,7 +724,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.zoom-in',
|
||||
kbd: '$=',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'zoom-in')
|
||||
app.zoomIn(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
},
|
||||
|
@ -717,7 +734,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.zoom-out',
|
||||
kbd: '$-',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'zoom-out')
|
||||
app.zoomOut(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
},
|
||||
|
@ -727,7 +745,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
icon: 'reset-zoom',
|
||||
kbd: '!0',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'reset-zoom')
|
||||
app.resetZoom(app.viewportScreenCenter, { duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
},
|
||||
|
@ -736,7 +755,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.zoom-to-fit',
|
||||
kbd: '!1',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'zoom-to-fit')
|
||||
app.zoomToFit({ duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
},
|
||||
|
@ -745,7 +765,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.zoom-to-selection',
|
||||
kbd: '!2',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'zoom-to-selection')
|
||||
app.zoomToSelection({ duration: ANIMATION_MEDIUM_MS })
|
||||
},
|
||||
},
|
||||
|
@ -754,13 +775,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.toggle-snap-mode',
|
||||
menuLabel: 'action.toggle-snap-mode.menu',
|
||||
readonlyOk: false,
|
||||
onSelect() {
|
||||
app.updateUserDocumentSettings(
|
||||
{
|
||||
isSnapMode: !app.userDocumentSettings.isSnapMode,
|
||||
},
|
||||
true
|
||||
)
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-snap-mode')
|
||||
app.setSnapMode(!app.isSnapMode)
|
||||
},
|
||||
checkbox: true,
|
||||
},
|
||||
|
@ -770,13 +787,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.toggle-dark-mode.menu',
|
||||
kbd: '$/',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
app.updateUserDocumentSettings(
|
||||
{
|
||||
isDarkMode: !app.userDocumentSettings.isDarkMode,
|
||||
},
|
||||
true
|
||||
)
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-dark-mode')
|
||||
app.setDarkMode(!app.isDarkMode)
|
||||
},
|
||||
checkbox: true,
|
||||
},
|
||||
|
@ -786,7 +799,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.toggle-transparent.menu',
|
||||
contextMenuLabel: 'action.toggle-transparent.context-menu',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-transparent')
|
||||
app.updateInstanceState(
|
||||
{
|
||||
exportBackground: !app.instanceState.exportBackground,
|
||||
|
@ -802,13 +816,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.toggle-tool-lock.menu',
|
||||
readonlyOk: false,
|
||||
kbd: 'q',
|
||||
onSelect() {
|
||||
app.updateInstanceState(
|
||||
{
|
||||
isToolLocked: !app.instanceState.isToolLocked,
|
||||
},
|
||||
true
|
||||
)
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-tool-lock')
|
||||
app.setToolLocked(!app.isToolLocked)
|
||||
},
|
||||
checkbox: true,
|
||||
},
|
||||
|
@ -819,19 +829,15 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: true,
|
||||
kbd: '$.',
|
||||
checkbox: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
// this needs to be deferred because it causes the menu
|
||||
// UI to unmount which puts us in a dodgy state
|
||||
requestAnimationFrame(() => {
|
||||
app.batch(() => {
|
||||
trackEvent(source, 'toggle-focus-mode')
|
||||
clearDialogs()
|
||||
clearToasts()
|
||||
app.updateInstanceState(
|
||||
{
|
||||
isFocusMode: !app.instanceState.isFocusMode,
|
||||
},
|
||||
true
|
||||
)
|
||||
app.setFocusMode(!app.isFocusMode)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@ -842,13 +848,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
menuLabel: 'action.toggle-grid.menu',
|
||||
readonlyOk: true,
|
||||
kbd: "$'",
|
||||
onSelect() {
|
||||
app.updateUserDocumentSettings(
|
||||
{
|
||||
isGridMode: !app.userDocumentSettings.isGridMode,
|
||||
},
|
||||
true
|
||||
)
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-grid-mode')
|
||||
app.setGridMode(!app.isGridMode)
|
||||
},
|
||||
checkbox: true,
|
||||
},
|
||||
|
@ -857,7 +859,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.toggle-debug-mode',
|
||||
menuLabel: 'action.toggle-debug-mode.menu',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'toggle-debug-mode')
|
||||
app.updateInstanceState(
|
||||
{
|
||||
isDebugMode: !app.instanceState.isDebugMode,
|
||||
|
@ -872,7 +875,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.print',
|
||||
kbd: '$p',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'print')
|
||||
printSelectionOrPages()
|
||||
},
|
||||
},
|
||||
|
@ -881,7 +885,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.exit-pen-mode',
|
||||
icon: 'cross-2',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'exit-pen-mode')
|
||||
app.setPenMode(false)
|
||||
},
|
||||
},
|
||||
|
@ -890,7 +895,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.stop-following',
|
||||
icon: 'cross-2',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'stop-following')
|
||||
app.stopFollowingUser()
|
||||
},
|
||||
},
|
||||
|
@ -899,19 +905,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
label: 'action.back-to-content',
|
||||
icon: 'arrow-left',
|
||||
readonlyOk: true,
|
||||
onSelect() {
|
||||
const bounds = app.selectedPageBounds ?? app.allShapesCommonBounds
|
||||
|
||||
if (bounds) {
|
||||
app.zoomToBounds(
|
||||
bounds.minX,
|
||||
bounds.minY,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
Math.min(1, app.zoomLevel),
|
||||
{ duration: 220 }
|
||||
)
|
||||
}
|
||||
onSelect(source) {
|
||||
trackEvent(source, 'zoom-to-content')
|
||||
app.zoomToContent()
|
||||
},
|
||||
},
|
||||
])
|
||||
|
@ -922,6 +918,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
return actions
|
||||
}, [
|
||||
trackEvent,
|
||||
overrides,
|
||||
app,
|
||||
addDialog,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TLPageId, useApp } from '@tldraw/editor'
|
||||
import { useApp } from '@tldraw/editor'
|
||||
import { useEffect } from 'react'
|
||||
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('moved-to-page', handleMoveToPage)
|
||||
return () => {
|
||||
app.removeListener('max-shapes', handleMaxShapes)
|
||||
app.removeListener('moved-to-page', handleMoveToPage)
|
||||
}
|
||||
}, [app, addToast])
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import { compact, isNonNull } from '@tldraw/utils'
|
|||
import { compressToBase64, decompressFromBase64 } from 'lz-string'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useAppIsFocused } from './useAppIsFocused'
|
||||
import { useEvents } from './useEventsProvider'
|
||||
|
||||
/** @public */
|
||||
export type EmbedInfo = {
|
||||
|
@ -969,22 +970,27 @@ const handleNativeClipboardPaste = async (
|
|||
/** @public */
|
||||
export function useMenuClipboardEvents() {
|
||||
const app = useApp()
|
||||
const trackEvent = useEvents()
|
||||
|
||||
const copy = useCallback(
|
||||
function onCopy() {
|
||||
if (app.selectedIds.length === 0) return
|
||||
|
||||
handleMenuCopy(app)
|
||||
trackEvent('menu', 'copy')
|
||||
},
|
||||
[app]
|
||||
[app, trackEvent]
|
||||
)
|
||||
|
||||
const cut = useCallback(
|
||||
function onCut() {
|
||||
if (app.selectedIds.length === 0) return
|
||||
|
||||
handleMenuCopy(app)
|
||||
app.deleteShapes()
|
||||
trackEvent('menu', 'cut')
|
||||
},
|
||||
[app]
|
||||
[app, trackEvent]
|
||||
)
|
||||
|
||||
const paste = useCallback(
|
||||
|
@ -1000,8 +1006,10 @@ export function useMenuClipboardEvents() {
|
|||
// else {
|
||||
// handleScenePaste(app, point)
|
||||
// }
|
||||
|
||||
trackEvent('menu', 'paste')
|
||||
},
|
||||
[app]
|
||||
[app, trackEvent]
|
||||
)
|
||||
|
||||
return {
|
||||
|
@ -1014,6 +1022,7 @@ export function useMenuClipboardEvents() {
|
|||
/** @public */
|
||||
export function useNativeClipboardEvents() {
|
||||
const app = useApp()
|
||||
const trackEvent = useEvents()
|
||||
|
||||
const appIsFocused = useAppIsFocused()
|
||||
|
||||
|
@ -1023,6 +1032,7 @@ export function useNativeClipboardEvents() {
|
|||
if (app.selectedIds.length === 0 || app.editingId !== null || disallowClipboardEvents(app))
|
||||
return
|
||||
handleMenuCopy(app)
|
||||
trackEvent('kbd', 'copy')
|
||||
}
|
||||
|
||||
function cut() {
|
||||
|
@ -1030,12 +1040,13 @@ export function useNativeClipboardEvents() {
|
|||
return
|
||||
handleMenuCopy(app)
|
||||
app.deleteShapes()
|
||||
trackEvent('kbd', 'cut')
|
||||
}
|
||||
|
||||
const paste = (event: ClipboardEvent) => {
|
||||
const paste = (e: ClipboardEvent) => {
|
||||
if (app.editingId !== null || disallowClipboardEvents(app)) return
|
||||
if (event.clipboardData && !app.inputs.shiftKey) {
|
||||
handleNativeDataTransferPaste(app, event.clipboardData)
|
||||
if (e.clipboardData && !app.inputs.shiftKey) {
|
||||
handleNativeDataTransferPaste(app, e.clipboardData)
|
||||
} else {
|
||||
navigator.clipboard.read().then((clipboardItems) => {
|
||||
if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) {
|
||||
|
@ -1043,6 +1054,7 @@ export function useNativeClipboardEvents() {
|
|||
}
|
||||
})
|
||||
}
|
||||
trackEvent('kbd', 'paste')
|
||||
}
|
||||
|
||||
document.addEventListener('copy', copy)
|
||||
|
@ -1054,5 +1066,5 @@ export function useNativeClipboardEvents() {
|
|||
document.removeEventListener('cut', cut)
|
||||
document.removeEventListener('paste', paste)
|
||||
}
|
||||
}, [app, appIsFocused])
|
||||
}, [app, trackEvent, appIsFocused])
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { App, uniqueId, useApp } from '@tldraw/editor'
|
||||
import { createContext, useCallback, useContext, useState } from 'react'
|
||||
import { useEvents } from './useEventsProvider'
|
||||
|
||||
/** @public */
|
||||
export interface DialogProps {
|
||||
|
@ -34,6 +35,7 @@ export type DialogsProviderProps = {
|
|||
/** @public */
|
||||
export function DialogsProvider({ children }: DialogsProviderProps) {
|
||||
const app = useApp()
|
||||
const trackEvent = useEvents()
|
||||
|
||||
const [dialogs, setDialogs] = useState<TLDialog[]>([])
|
||||
|
||||
|
@ -44,11 +46,12 @@ export function DialogsProvider({ children }: DialogsProviderProps) {
|
|||
return [...d.filter((m) => m.id !== dialog.id), { ...dialog, id }]
|
||||
})
|
||||
|
||||
app.openMenus.add(id)
|
||||
trackEvent('dialog', 'open-menu', { id })
|
||||
app.addOpenMenu(id)
|
||||
|
||||
return id
|
||||
},
|
||||
[app]
|
||||
[app, trackEvent]
|
||||
)
|
||||
|
||||
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
|
||||
},
|
||||
[app]
|
||||
[app, trackEvent]
|
||||
)
|
||||
|
||||
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
|
||||
},
|
||||
[app]
|
||||
[app, trackEvent]
|
||||
)
|
||||
|
||||
const clearDialogs = useCallback(() => {
|
||||
setDialogs((d) => {
|
||||
d.forEach((m) => {
|
||||
m.onClose?.()
|
||||
app.openMenus.delete(m.id)
|
||||
trackEvent('dialog', 'close-menu', { id: m.id })
|
||||
app.deleteOpenMenu(m.id)
|
||||
})
|
||||
return []
|
||||
})
|
||||
}, [app])
|
||||
}, [app, trackEvent])
|
||||
|
||||
return (
|
||||
<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)
|
||||
|
||||
downloadDataURLAsFile(dataURL, `${name || 'shapes'}.${format}`)
|
||||
|
||||
URL.revokeObjectURL(dataURL)
|
||||
return
|
||||
}
|
||||
|
@ -83,6 +84,7 @@ export function useExportAs() {
|
|||
)
|
||||
|
||||
downloadDataURLAsFile(dataURL, `${name || 'shapes'}.json`)
|
||||
|
||||
URL.revokeObjectURL(dataURL)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ const SKIP_KBDS = [
|
|||
'copy',
|
||||
'cut',
|
||||
'paste',
|
||||
'delete',
|
||||
// There's also an upload asset action, so we don't want to set the kbd twice
|
||||
'asset',
|
||||
]
|
||||
|
@ -45,7 +46,7 @@ export function useKeyboardShortcuts() {
|
|||
hot(getHotkeysStringFromKbd(action.kbd), (event) => {
|
||||
if (areShortcutsDisabled()) return
|
||||
preventDefault(event)
|
||||
action.onSelect()
|
||||
action.onSelect('kbd')
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -69,19 +70,19 @@ export function useKeyboardShortcuts() {
|
|||
app.setSelectedTool('geo')
|
||||
})
|
||||
|
||||
hot('backspace,del', () => {
|
||||
hot('del,backspace', () => {
|
||||
if (areShortcutsDisabled()) return
|
||||
actions['delete'].onSelect()
|
||||
actions['delete'].onSelect('kbd')
|
||||
})
|
||||
|
||||
hot('=', () => {
|
||||
if (areShortcutsDisabled()) return
|
||||
actions['zoom-in'].onSelect()
|
||||
actions['zoom-in'].onSelect('kbd')
|
||||
})
|
||||
|
||||
hot('-', () => {
|
||||
if (areShortcutsDisabled()) return
|
||||
actions['zoom-out'].onSelect()
|
||||
actions['zoom-out'].onSelect('kbd')
|
||||
})
|
||||
|
||||
hotkeys.setScope(app.instanceId)
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
import { useApp } from '@tldraw/editor'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useEvents } from './useEventsProvider'
|
||||
|
||||
/** @public */
|
||||
export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
|
||||
const app = useApp()
|
||||
const rIsOpen = useRef(false)
|
||||
const trackEvent = useEvents()
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
rIsOpen.current = isOpen
|
||||
if (isOpen) {
|
||||
app.complete()
|
||||
app.openMenus.add(id)
|
||||
} else {
|
||||
app.openMenus.delete(id)
|
||||
app.openMenus.forEach((menuId) => {
|
||||
if (menuId.startsWith(id)) {
|
||||
app.openMenus.delete(menuId)
|
||||
}
|
||||
})
|
||||
}
|
||||
app.batch(() => {
|
||||
if (isOpen) {
|
||||
app.complete()
|
||||
app.addOpenMenu(id)
|
||||
} else {
|
||||
app.deleteOpenMenu(id)
|
||||
app.openMenus.forEach((menuId) => {
|
||||
if (menuId.startsWith(id)) {
|
||||
app.deleteOpenMenu(menuId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
cb?.(isOpen)
|
||||
cb?.(isOpen)
|
||||
})
|
||||
},
|
||||
[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
|
||||
// this effect runs twice or re-runs.
|
||||
if (rIsOpen.current) {
|
||||
app.openMenus.add(id)
|
||||
trackEvent('menu', 'open-menu', { id })
|
||||
app.addOpenMenu(id)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rIsOpen.current) {
|
||||
// Close menu on unmount
|
||||
app.openMenus.delete(id)
|
||||
app.deleteOpenMenu(id)
|
||||
|
||||
// Close menu and all submenus when the parent is closed
|
||||
app.openMenus.forEach((menuId) => {
|
||||
if (menuId.startsWith(id)) {
|
||||
app.openMenus.delete(menuId)
|
||||
trackEvent('menu', 'close-menu', { id })
|
||||
app.deleteOpenMenu(menuId)
|
||||
}
|
||||
})
|
||||
|
||||
rIsOpen.current = false
|
||||
}
|
||||
}
|
||||
}, [app, id])
|
||||
}, [app, id, trackEvent])
|
||||
|
||||
return onOpenChange
|
||||
}
|
||||
|
|
|
@ -129,8 +129,7 @@ export function usePrint() {
|
|||
}
|
||||
|
||||
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-camera', () => {
|
||||
app.once('change-history', () => {
|
||||
clearElements(el, style)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react'
|
|||
import { EmbedDialog } from '../components/EmbedDialog'
|
||||
import { TLUiIconType } from '../icon-types'
|
||||
import { useDialogs } from './useDialogsProvider'
|
||||
import { useEvents } from './useEventsProvider'
|
||||
import { useInsertMedia } from './useInsertMedia'
|
||||
import { TLTranslationKey } from './useTranslation/TLTranslationKey'
|
||||
|
||||
|
@ -39,6 +40,7 @@ export type ToolsProviderProps = {
|
|||
/** @public */
|
||||
export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
||||
const app = useApp()
|
||||
const trackEvent = useEvents()
|
||||
|
||||
const { addDialog } = useDialogs()
|
||||
const insertMedia = useInsertMedia()
|
||||
|
@ -53,6 +55,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect() {
|
||||
app.setSelectedTool('select')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'select' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -63,6 +66,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect() {
|
||||
app.setSelectedTool('hand')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'hand' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -73,6 +77,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
readonlyOk: true,
|
||||
onSelect() {
|
||||
app.setSelectedTool('eraser')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'eraser' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -83,6 +88,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: 'd,b,x',
|
||||
onSelect() {
|
||||
app.setSelectedTool('draw')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'draw' })
|
||||
},
|
||||
},
|
||||
...[...TL_GEO_TYPES].map((id) => ({
|
||||
|
@ -101,6 +107,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
true
|
||||
)
|
||||
app.setSelectedTool('geo')
|
||||
trackEvent('toolbar', 'select-tool', { id: `geo-${id}` })
|
||||
})
|
||||
},
|
||||
})),
|
||||
|
@ -112,6 +119,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: 'a',
|
||||
onSelect() {
|
||||
app.setSelectedTool('arrow')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'arrow' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -122,6 +130,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: 'l',
|
||||
onSelect() {
|
||||
app.setSelectedTool('line')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'line' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -132,6 +141,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: 'f',
|
||||
onSelect() {
|
||||
app.setSelectedTool('frame')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'frame' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -142,6 +152,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: 't',
|
||||
onSelect() {
|
||||
app.setSelectedTool('text')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'text' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -152,6 +163,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: '$u',
|
||||
onSelect() {
|
||||
insertMedia()
|
||||
trackEvent('toolbar', 'select-tool', { id: 'media' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -162,6 +174,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
kbd: 'n',
|
||||
onSelect() {
|
||||
app.setSelectedTool('note')
|
||||
trackEvent('toolbar', 'select-tool', { id: 'note' })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -171,6 +184,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
icon: 'tool-embed',
|
||||
onSelect() {
|
||||
addDialog({ component: EmbedDialog })
|
||||
trackEvent('toolbar', 'select-tool', { id: 'embed' })
|
||||
},
|
||||
},
|
||||
])
|
||||
|
@ -180,7 +194,7 @@ export function ToolsProvider({ overrides, children }: ToolsProviderProps) {
|
|||
}
|
||||
|
||||
return tools
|
||||
}, [app, overrides, insertMedia, addDialog])
|
||||
}, [app, trackEvent, overrides, insertMedia, addDialog])
|
||||
|
||||
return <ToolsContext.Provider value={tools}>{children}</ToolsContext.Provider>
|
||||
}
|
||||
|
|
|
@ -5891,6 +5891,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 6.7.1
|
||||
resolution: "@vercel/build-utils@npm:6.7.1"
|
||||
|
@ -10819,6 +10826,7 @@ __metadata:
|
|||
"@babel/plugin-proposal-decorators": ^7.21.0
|
||||
"@tldraw/assets": "workspace:*"
|
||||
"@tldraw/tldraw": "workspace:*"
|
||||
"@vercel/analytics": ^1.0.1
|
||||
lazyrepo: 0.0.0-alpha.26
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
|
|
Ładowanie…
Reference in New Issue