More cleanup, focus bug fixes (#1749)
This PR is another grab bag: - renames `readOnly` to `readonly` throughout editor - fixes a regression related to focus and keyboard shortcuts - adds a small outline for focused editors ### Change Type - [x] `major` ### Test Plan - [x] End to end tests
This commit is contained in:
parent
6309cbe6a5
commit
b22ea7cd4e
39 changed files with 350 additions and 176 deletions
119
apps/examples/e2e/tests/test-focus.spec.ts
Normal file
119
apps/examples/e2e/tests/test-focus.spec.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import test, { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
declare const __tldraw_editor_events: any[]
|
||||||
|
|
||||||
|
// We're just testing the events, not the actual results.
|
||||||
|
|
||||||
|
test.describe('Focus', () => {
|
||||||
|
test('focus events', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5420/multiple')
|
||||||
|
await page.waitForSelector('.tl-canvas')
|
||||||
|
|
||||||
|
// Component A has autofocus
|
||||||
|
// Component B does not
|
||||||
|
|
||||||
|
const EditorA = (await page.$(`.A`))!
|
||||||
|
const EditorB = (await page.$(`.B`))!
|
||||||
|
expect(EditorA).toBeTruthy()
|
||||||
|
expect(EditorB).toBeTruthy()
|
||||||
|
|
||||||
|
await (await page.$('body'))?.click()
|
||||||
|
|
||||||
|
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||||
|
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
|
||||||
|
await (await page.$('body'))?.click()
|
||||||
|
|
||||||
|
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
|
||||||
|
await EditorA.click()
|
||||||
|
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||||
|
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
|
||||||
|
await EditorA.click()
|
||||||
|
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
expect(await EditorA.evaluate((node) => node.contains(document.activeElement))).toBe(true)
|
||||||
|
|
||||||
|
await EditorB.click()
|
||||||
|
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
expect(await EditorA.evaluate((node) => node.contains(document.activeElement))).toBe(true)
|
||||||
|
|
||||||
|
// Escape does not break focus
|
||||||
|
await page.keyboard.press('Escape')
|
||||||
|
expect(await EditorA.evaluate((node) => node.contains(document.activeElement))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('kbds when focused', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5420/multiple')
|
||||||
|
await page.waitForSelector('.tl-canvas')
|
||||||
|
|
||||||
|
const EditorA = (await page.$(`.A`))!
|
||||||
|
const EditorB = (await page.$(`.B`))!
|
||||||
|
expect(EditorA).toBeTruthy()
|
||||||
|
expect(EditorB).toBeTruthy()
|
||||||
|
|
||||||
|
await (await page.$('body'))?.click()
|
||||||
|
|
||||||
|
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||||
|
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||||
|
|
||||||
|
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.keyboard.press('d')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')
|
||||||
|
).not.toBe(null)
|
||||||
|
expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
await EditorB.click()
|
||||||
|
await page.waitForTimeout(100) // takes 30ms or so to focus
|
||||||
|
await page.keyboard.press('d')
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')
|
||||||
|
).not.toBe(null)
|
||||||
|
expect(
|
||||||
|
await EditorB.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')
|
||||||
|
).not.toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('kbds after clicking on ui elements', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5420/end-to-end')
|
||||||
|
await page.waitForSelector('.tl-canvas')
|
||||||
|
|
||||||
|
const EditorA = (await page.$(`.tl-container`))!
|
||||||
|
expect(EditorA).toBeTruthy()
|
||||||
|
|
||||||
|
const drawButton = await EditorA.$('.tlui-button[data-testid="tools.draw"]')
|
||||||
|
|
||||||
|
// select button should be selected, not the draw button
|
||||||
|
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
await drawButton?.click()
|
||||||
|
|
||||||
|
// draw button should be selected now
|
||||||
|
expect(
|
||||||
|
await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')
|
||||||
|
).not.toBe(null)
|
||||||
|
|
||||||
|
await page.keyboard.press('v')
|
||||||
|
|
||||||
|
// select button should be selected again
|
||||||
|
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -12,7 +12,7 @@ export default function MultipleExample() {
|
||||||
<h2>First Example</h2>
|
<h2>First Example</h2>
|
||||||
<p>This is the second example.</p>
|
<p>This is the second example.</p>
|
||||||
<div style={{ width: '100%', height: '600px', padding: 32 }} tabIndex={-1}>
|
<div style={{ width: '100%', height: '600px', padding: 32 }} tabIndex={-1}>
|
||||||
<Tldraw persistenceKey="steve" autoFocus />
|
<Tldraw persistenceKey="steve" className="A" autoFocus />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<textarea defaultValue="type in me" style={{ margin: 10 }}></textarea>
|
<textarea defaultValue="type in me" style={{ margin: 10 }}></textarea>
|
||||||
|
@ -20,7 +20,7 @@ export default function MultipleExample() {
|
||||||
<h2>Second Example</h2>
|
<h2>Second Example</h2>
|
||||||
<p>This is the second example.</p>
|
<p>This is the second example.</p>
|
||||||
<div style={{ width: '100%', height: '600px' }} tabIndex={-1}>
|
<div style={{ width: '100%', height: '600px' }} tabIndex={-1}>
|
||||||
<Tldraw persistenceKey="david" autoFocus={false} />
|
<Tldraw persistenceKey="david" className="B" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
"page-menu.submenu.duplicate-page": "Duplicar",
|
"page-menu.submenu.duplicate-page": "Duplicar",
|
||||||
"page-menu.submenu.delete": "Deletar",
|
"page-menu.submenu.delete": "Deletar",
|
||||||
"share-menu.copy-link": "Copiar Link de Convite",
|
"share-menu.copy-link": "Copiar Link de Convite",
|
||||||
"share-menu.copy-readonly-link": "Copiar Link ReadOnly",
|
"share-menu.copy-readonly-link": "Copiar Link Readonly",
|
||||||
"help-menu.keyboard-shortcuts": "Atalhos de Teclado",
|
"help-menu.keyboard-shortcuts": "Atalhos de Teclado",
|
||||||
"edit-link-dialog.cancel": "Cancelar",
|
"edit-link-dialog.cancel": "Cancelar",
|
||||||
"embed-dialog.cancel": "Cancelar",
|
"embed-dialog.cancel": "Cancelar",
|
||||||
|
|
|
@ -1643,6 +1643,7 @@ export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
|
||||||
export interface TldrawEditorBaseProps {
|
export interface TldrawEditorBaseProps {
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
children?: any;
|
children?: any;
|
||||||
|
className?: string;
|
||||||
components?: Partial<TLEditorComponents>;
|
components?: Partial<TLEditorComponents>;
|
||||||
initialState?: string;
|
initialState?: string;
|
||||||
onMount?: TLOnMountHandler;
|
onMount?: TLOnMountHandler;
|
||||||
|
|
|
@ -191,6 +191,10 @@
|
||||||
-webkit-touch-callout: initial;
|
-webkit-touch-callout: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-container:focus-within {
|
||||||
|
outline: 1px solid var(--color-low);
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
*[contenteditable],
|
*[contenteditable],
|
||||||
*[contenteditable] * {
|
*[contenteditable] * {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import React, {
|
||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
|
import classNames from 'classnames'
|
||||||
import { Canvas } from './components/Canvas'
|
import { Canvas } from './components/Canvas'
|
||||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||||
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
||||||
|
@ -92,6 +93,11 @@ export interface TldrawEditorBaseProps {
|
||||||
* The editor's initial state (usually the id of the first active tool).
|
* The editor's initial state (usually the id of the first active tool).
|
||||||
*/
|
*/
|
||||||
initialState?: string
|
initialState?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A classname to pass to the editor's container.
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,9 +125,10 @@ const EMPTY_TOOLS_ARRAY = [] as const
|
||||||
export const TldrawEditor = memo(function TldrawEditor({
|
export const TldrawEditor = memo(function TldrawEditor({
|
||||||
store,
|
store,
|
||||||
components,
|
components,
|
||||||
|
className,
|
||||||
...rest
|
...rest
|
||||||
}: TldrawEditorProps) {
|
}: TldrawEditorProps) {
|
||||||
const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
|
const [container, rContainer] = React.useState<HTMLDivElement | null>(null)
|
||||||
const user = useMemo(() => createTLUser(), [])
|
const user = useMemo(() => createTLUser(), [])
|
||||||
|
|
||||||
const ErrorFallback =
|
const ErrorFallback =
|
||||||
|
@ -137,7 +144,12 @@ export const TldrawEditor = memo(function TldrawEditor({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}>
|
<div
|
||||||
|
ref={rContainer}
|
||||||
|
draggable={false}
|
||||||
|
className={classNames('tl-container tl-theme__light', className)}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<OptionalErrorBoundary
|
<OptionalErrorBoundary
|
||||||
fallback={ErrorFallback}
|
fallback={ErrorFallback}
|
||||||
onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })}
|
onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })}
|
||||||
|
|
|
@ -188,9 +188,9 @@ function HandlesWrapper() {
|
||||||
const isChangingStyle = useValue('isChangingStyle', () => editor.instanceState.isChangingStyle, [
|
const isChangingStyle = useValue('isChangingStyle', () => editor.instanceState.isChangingStyle, [
|
||||||
editor,
|
editor,
|
||||||
])
|
])
|
||||||
const isReadOnly = useValue('isChangingStyle', () => editor.instanceState.isReadOnly, [editor])
|
const isReadonly = useValue('isChangingStyle', () => editor.instanceState.isReadonly, [editor])
|
||||||
|
|
||||||
if (!Handles || !onlySelectedShape || isChangingStyle || isReadOnly) return null
|
if (!Handles || !onlySelectedShape || isChangingStyle || isReadonly) return null
|
||||||
|
|
||||||
const handles = editor.getHandles(onlySelectedShape)
|
const handles = editor.getHandles(onlySelectedShape)
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ import {
|
||||||
annotateError,
|
annotateError,
|
||||||
assert,
|
assert,
|
||||||
compact,
|
compact,
|
||||||
|
debounce,
|
||||||
dedupe,
|
dedupe,
|
||||||
deepCopy,
|
deepCopy,
|
||||||
getOwnProperty,
|
getOwnProperty,
|
||||||
|
@ -306,19 +307,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
const container = this.getContainer()
|
const container = this.getContainer()
|
||||||
|
|
||||||
const handleFocus = () => this.updateInstanceState({ isFocused: true })
|
// We need to debounce this because when focus changes, the body
|
||||||
const handleBlur = () => this.updateInstanceState({ isFocused: false })
|
// becomes focused for a brief moment. Debouncing means that we
|
||||||
|
// check only when focus stops changing: when it settles, what
|
||||||
|
// has it settled on? If it's settled on the container or something
|
||||||
|
// inside of the container, then focus or preserve the current focus;
|
||||||
|
// if not, then turn off focus. Turning off focus is a trigger to
|
||||||
|
// also turn off keyboard shortcuts and other things.
|
||||||
|
const updateFocus = debounce(() => {
|
||||||
|
const { activeElement } = document
|
||||||
|
const { isFocused } = this.instanceState
|
||||||
|
const hasFocus = container === activeElement || container.contains(activeElement)
|
||||||
|
if ((!isFocused && hasFocus) || (isFocused && !hasFocus)) {
|
||||||
|
this.updateInstanceState({ isFocused: hasFocus })
|
||||||
|
}
|
||||||
|
}, 32)
|
||||||
|
|
||||||
container.addEventListener('focusin', handleFocus)
|
container.addEventListener('focusin', updateFocus)
|
||||||
container.addEventListener('focus', handleFocus)
|
container.addEventListener('focus', updateFocus)
|
||||||
container.addEventListener('focusout', handleBlur)
|
container.addEventListener('focusout', updateFocus)
|
||||||
container.addEventListener('blur', handleBlur)
|
container.addEventListener('blur', updateFocus)
|
||||||
|
|
||||||
this.disposables.add(() => {
|
this.disposables.add(() => {
|
||||||
container.removeEventListener('focusin', handleFocus)
|
container.removeEventListener('focusin', updateFocus)
|
||||||
container.removeEventListener('focus', handleFocus)
|
container.removeEventListener('focus', updateFocus)
|
||||||
container.removeEventListener('focusout', handleBlur)
|
container.removeEventListener('focusout', updateFocus)
|
||||||
container.removeEventListener('blur', handleBlur)
|
container.removeEventListener('blur', updateFocus)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.store.ensureStoreIsUsable()
|
this.store.ensureStoreIsUsable()
|
||||||
|
@ -1046,26 +1060,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return paths.some((path) => this.isIn(path))
|
return paths.some((path) => this.isIn(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The id of the current selected tool.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
@computed get currentToolId(): string {
|
|
||||||
const { currentTool } = this
|
|
||||||
if (!currentTool) return ''
|
|
||||||
return currentTool.currentToolIdMask ?? currentTool.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current selected tool.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
@computed get currentTool(): StateNode | undefined {
|
|
||||||
return this.root.current.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the selected tool.
|
* Set the selected tool.
|
||||||
*
|
*
|
||||||
|
@ -1084,6 +1078,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
this.root.transition(id, info)
|
this.root.transition(id, info)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* The current selected tool.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
@computed get currentTool(): StateNode | undefined {
|
||||||
|
return this.root.current.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the current selected tool.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
@computed get currentToolId(): string {
|
||||||
|
const { currentTool } = this
|
||||||
|
if (!currentTool) return ''
|
||||||
|
return currentTool.currentToolIdMask ?? currentTool.id
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a descendant by its path.
|
* Get a descendant by its path.
|
||||||
|
@ -3276,7 +3289,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _updatePage = this.history.createCommand(
|
private _updatePage = this.history.createCommand(
|
||||||
'updatePage',
|
'updatePage',
|
||||||
(partial: RequiredKeys<TLPage, 'id'>, squashing = false) => {
|
(partial: RequiredKeys<TLPage, 'id'>, squashing = false) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
|
|
||||||
const prev = this.getPageById(partial.id)
|
const prev = this.getPageById(partial.id)
|
||||||
|
|
||||||
|
@ -3326,7 +3339,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _createPage = this.history.createCommand(
|
private _createPage = this.history.createCommand(
|
||||||
'createPage',
|
'createPage',
|
||||||
(title: string, id: TLPageId = PageRecordType.createId(), belowPageIndex?: string) => {
|
(title: string, id: TLPageId = PageRecordType.createId(), belowPageIndex?: string) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
if (this.pages.length >= MAX_PAGES) return null
|
if (this.pages.length >= MAX_PAGES) return null
|
||||||
const pageInfo = this.pages
|
const pageInfo = this.pages
|
||||||
const topIndex = belowPageIndex ?? pageInfo[pageInfo.length - 1]?.index ?? 'a1'
|
const topIndex = belowPageIndex ?? pageInfo[pageInfo.length - 1]?.index ?? 'a1'
|
||||||
|
@ -3408,7 +3421,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _deletePage = this.history.createCommand(
|
private _deletePage = this.history.createCommand(
|
||||||
'delete_page',
|
'delete_page',
|
||||||
(id: TLPageId) => {
|
(id: TLPageId) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
const { pages } = this
|
const { pages } = this
|
||||||
if (pages.length === 1) return null
|
if (pages.length === 1) return null
|
||||||
|
|
||||||
|
@ -3492,7 +3505,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
renamePage(id: TLPageId, name: string, squashing = false) {
|
renamePage(id: TLPageId, name: string, squashing = false) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
this.updatePage({ id, name }, squashing)
|
this.updatePage({ id, name }, squashing)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -3533,7 +3546,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _createAssets = this.history.createCommand(
|
private _createAssets = this.history.createCommand(
|
||||||
'createAssets',
|
'createAssets',
|
||||||
(assets: TLAsset[]) => {
|
(assets: TLAsset[]) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
if (assets.length <= 0) return null
|
if (assets.length <= 0) return null
|
||||||
|
|
||||||
return { data: { assets } }
|
return { data: { assets } }
|
||||||
|
@ -3569,7 +3582,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _updateAssets = this.history.createCommand(
|
private _updateAssets = this.history.createCommand(
|
||||||
'updateAssets',
|
'updateAssets',
|
||||||
(assets: TLAssetPartial[]) => {
|
(assets: TLAssetPartial[]) => {
|
||||||
if (this.instanceState.isReadOnly) return
|
if (this.instanceState.isReadonly) return
|
||||||
if (assets.length <= 0) return
|
if (assets.length <= 0) return
|
||||||
|
|
||||||
const snapshots: Record<string, TLAsset> = {}
|
const snapshots: Record<string, TLAsset> = {}
|
||||||
|
@ -3616,7 +3629,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _deleteAssets = this.history.createCommand(
|
private _deleteAssets = this.history.createCommand(
|
||||||
'deleteAssets',
|
'deleteAssets',
|
||||||
(ids: TLAssetId[]) => {
|
(ids: TLAssetId[]) => {
|
||||||
if (this.instanceState.isReadOnly) return
|
if (this.instanceState.isReadonly) return
|
||||||
if (ids.length <= 0) return
|
if (ids.length <= 0) return
|
||||||
|
|
||||||
const prev = compact(ids.map((id) => this.store.get(id)))
|
const prev = compact(ids.map((id) => this.store.get(id)))
|
||||||
|
@ -5236,7 +5249,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this {
|
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this {
|
||||||
if (ids.length === 0) return this
|
if (ids.length === 0) return this
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
|
|
||||||
const { currentPageId } = this
|
const { currentPageId } = this
|
||||||
|
|
||||||
|
@ -5293,7 +5306,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
toggleLock(ids: TLShapeId[] = this.selectedIds): this {
|
toggleLock(ids: TLShapeId[] = this.selectedIds): this {
|
||||||
if (this.instanceState.isReadOnly || ids.length === 0) return this
|
if (this.instanceState.isReadonly || ids.length === 0) return this
|
||||||
|
|
||||||
let allLocked = true,
|
let allLocked = true,
|
||||||
allUnlocked = true
|
allUnlocked = true
|
||||||
|
@ -5414,7 +5427,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
flipShapes(operation: 'horizontal' | 'vertical', ids: TLShapeId[] = this.selectedIds) {
|
flipShapes(operation: 'horizontal' | 'vertical', ids: TLShapeId[] = this.selectedIds) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
|
|
||||||
let shapes = compact(ids.map((id) => this.getShapeById(id)))
|
let shapes = compact(ids.map((id) => this.getShapeById(id)))
|
||||||
|
|
||||||
|
@ -5478,7 +5491,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
ids: TLShapeId[] = this.currentPageState.selectedIds,
|
ids: TLShapeId[] = this.currentPageState.selectedIds,
|
||||||
gap?: number
|
gap?: number
|
||||||
) {
|
) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
|
|
||||||
const shapes = compact(ids.map((id) => this.getShapeById(id))).filter((shape) => {
|
const shapes = compact(ids.map((id) => this.getShapeById(id))).filter((shape) => {
|
||||||
if (!shape) return false
|
if (!shape) return false
|
||||||
|
@ -5605,7 +5618,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @param padding - The padding to apply to the packed shapes.
|
* @param padding - The padding to apply to the packed shapes.
|
||||||
*/
|
*/
|
||||||
packShapes(ids: TLShapeId[] = this.currentPageState.selectedIds, padding = 16) {
|
packShapes(ids: TLShapeId[] = this.currentPageState.selectedIds, padding = 16) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
if (ids.length < 2) return this
|
if (ids.length < 2) return this
|
||||||
|
|
||||||
const shapes = compact(
|
const shapes = compact(
|
||||||
|
@ -5764,7 +5777,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom',
|
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom',
|
||||||
ids: TLShapeId[] = this.currentPageState.selectedIds
|
ids: TLShapeId[] = this.currentPageState.selectedIds
|
||||||
) {
|
) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
if (ids.length < 2) return this
|
if (ids.length < 2) return this
|
||||||
|
|
||||||
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
||||||
|
@ -5851,7 +5864,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
operation: 'horizontal' | 'vertical',
|
operation: 'horizontal' | 'vertical',
|
||||||
ids: TLShapeId[] = this.currentPageState.selectedIds
|
ids: TLShapeId[] = this.currentPageState.selectedIds
|
||||||
) {
|
) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
if (ids.length < 3) return this
|
if (ids.length < 3) return this
|
||||||
|
|
||||||
const len = ids.length
|
const len = ids.length
|
||||||
|
@ -5936,7 +5949,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
operation: 'horizontal' | 'vertical',
|
operation: 'horizontal' | 'vertical',
|
||||||
ids: TLShapeId[] = this.currentPageState.selectedIds
|
ids: TLShapeId[] = this.currentPageState.selectedIds
|
||||||
) {
|
) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
if (ids.length < 2) return this
|
if (ids.length < 2) return this
|
||||||
|
|
||||||
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
||||||
|
@ -6024,7 +6037,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
mode?: TLResizeMode
|
mode?: TLResizeMode
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
|
|
||||||
if (!Number.isFinite(scale.x)) scale = new Vec2d(1, scale.y)
|
if (!Number.isFinite(scale.x)) scale = new Vec2d(1, scale.y)
|
||||||
if (!Number.isFinite(scale.y)) scale = new Vec2d(scale.x, 1)
|
if (!Number.isFinite(scale.y)) scale = new Vec2d(scale.x, 1)
|
||||||
|
@ -6296,7 +6309,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _createShapes = this.history.createCommand(
|
private _createShapes = this.history.createCommand(
|
||||||
'createShapes',
|
'createShapes',
|
||||||
(partials: TLShapePartial[], select = false) => {
|
(partials: TLShapePartial[], select = false) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
if (partials.length <= 0) return null
|
if (partials.length <= 0) return null
|
||||||
|
|
||||||
const { currentPageShapeIds: shapeIds, selectedIds } = this
|
const { currentPageShapeIds: shapeIds, selectedIds } = this
|
||||||
|
@ -6584,7 +6597,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
groupShapes(ids: TLShapeId[] = this.selectedIds, groupId = createShapeId()) {
|
groupShapes(ids: TLShapeId[] = this.selectedIds, groupId = createShapeId()) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
|
|
||||||
if (ids.length <= 1) return this
|
if (ids.length <= 1) return this
|
||||||
|
|
||||||
|
@ -6639,7 +6652,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
ungroupShapes(ids: TLShapeId[] = this.selectedIds) {
|
ungroupShapes(ids: TLShapeId[] = this.selectedIds) {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
if (ids.length === 0) return this
|
if (ids.length === 0) return this
|
||||||
|
|
||||||
// Only ungroup when the select tool is active
|
// Only ungroup when the select tool is active
|
||||||
|
@ -6730,7 +6743,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _updateShapes = this.history.createCommand(
|
private _updateShapes = this.history.createCommand(
|
||||||
'updateShapes',
|
'updateShapes',
|
||||||
(_partials: (TLShapePartial | null | undefined)[], squashing = false) => {
|
(_partials: (TLShapePartial | null | undefined)[], squashing = false) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
|
|
||||||
const partials = compact(_partials)
|
const partials = compact(_partials)
|
||||||
|
|
||||||
|
@ -6853,7 +6866,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _deleteShapes = this.history.createCommand(
|
private _deleteShapes = this.history.createCommand(
|
||||||
'delete_shapes',
|
'delete_shapes',
|
||||||
(ids: TLShapeId[]) => {
|
(ids: TLShapeId[]) => {
|
||||||
if (this.instanceState.isReadOnly) return null
|
if (this.instanceState.isReadonly) return null
|
||||||
if (ids.length === 0) return null
|
if (ids.length === 0) return null
|
||||||
const prevSelectedIds = [...this.currentPageState.selectedIds]
|
const prevSelectedIds = [...this.currentPageState.selectedIds]
|
||||||
|
|
||||||
|
@ -7444,7 +7457,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
preserveIds?: boolean
|
preserveIds?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
): this {
|
): this {
|
||||||
if (this.instanceState.isReadOnly) return this
|
if (this.instanceState.isReadonly) return this
|
||||||
|
|
||||||
if (!content.schema) {
|
if (!content.schema) {
|
||||||
throw Error('Could not put content:\ncontent is missing a schema.')
|
throw Error('Could not put content:\ncontent is missing a schema.')
|
||||||
|
|
|
@ -233,15 +233,17 @@ export class SnapManager {
|
||||||
constructor(public readonly editor: Editor) {}
|
constructor(public readonly editor: Editor) {}
|
||||||
|
|
||||||
@computed get snapPointsCache() {
|
@computed get snapPointsCache() {
|
||||||
return this.editor.store.createComputedCache<SnapPoint[], TLShape>('snapPoints', (shape) => {
|
const { editor } = this
|
||||||
const pageTransfrorm = this.editor.getPageTransformById(shape.id)
|
return editor.store.createComputedCache<SnapPoint[], TLShape>('snapPoints', (shape) => {
|
||||||
|
const pageTransfrorm = editor.getPageTransformById(shape.id)
|
||||||
if (!pageTransfrorm) return undefined
|
if (!pageTransfrorm) return undefined
|
||||||
const util = this.editor.getShapeUtil(shape)
|
return editor
|
||||||
const snapPoints = util.snapPoints(shape)
|
.getShapeUtil(shape)
|
||||||
return snapPoints.map((point, i) => {
|
.snapPoints(shape)
|
||||||
const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point)
|
.map((point, i) => {
|
||||||
return { x, y, id: `${shape.id}:${i}` }
|
const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point)
|
||||||
})
|
return { x, y, id: `${shape.id}:${i}` }
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { T } from '@tldraw/validate'
|
|
||||||
|
|
||||||
export type TLinstanceState = {
|
|
||||||
canMoveCamera: boolean
|
|
||||||
isFocused: boolean
|
|
||||||
devicePixelRatio: number
|
|
||||||
isCoarsePointer: boolean
|
|
||||||
openMenus: string[]
|
|
||||||
isChangingStyle: boolean
|
|
||||||
isReadOnly: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const instanceStateValidator = T.object<TLinstanceState>({
|
|
||||||
canMoveCamera: T.boolean,
|
|
||||||
isFocused: T.boolean,
|
|
||||||
devicePixelRatio: T.number,
|
|
||||||
isCoarsePointer: T.boolean,
|
|
||||||
openMenus: T.arrayOf(T.string),
|
|
||||||
isChangingStyle: T.boolean,
|
|
||||||
isReadOnly: T.boolean,
|
|
||||||
})
|
|
|
@ -80,7 +80,9 @@ export function useCanvasEvents() {
|
||||||
|
|
||||||
function onTouchStart(e: React.TouchEvent) {
|
function onTouchStart(e: React.TouchEvent) {
|
||||||
;(e as any).isKilled = true
|
;(e as any).isKilled = true
|
||||||
document.body.click() // god damn it, but necessary for long presses to open the context menu
|
// todo: investigate whether this effects keyboard shortcuts
|
||||||
|
// god damn it, but necessary for long presses to open the context menu
|
||||||
|
document.body.click()
|
||||||
preventDefault(e)
|
preventDefault(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ export function useDocumentEvents() {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
e.altKey &&
|
e.altKey &&
|
||||||
|
// todo: When should we allow the alt key to be used? Perhaps states should declare which keys matter to them?
|
||||||
(editor.isIn('zoom') || !editor.root.path.value.endsWith('.idle')) &&
|
(editor.isIn('zoom') || !editor.root.path.value.endsWith('.idle')) &&
|
||||||
!isFocusingInput()
|
!isFocusingInput()
|
||||||
) {
|
) {
|
||||||
|
@ -45,21 +46,14 @@ export function useDocumentEvents() {
|
||||||
;(e as any).isKilled = true
|
;(e as any).isKilled = true
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case '=': {
|
case '=':
|
||||||
if (e.metaKey || e.ctrlKey) {
|
case '-':
|
||||||
preventDefault(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case '-': {
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
preventDefault(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case '0': {
|
case '0': {
|
||||||
|
// These keys are used for zooming. Technically we only use
|
||||||
|
// the + - and 0 keys, however it's common for them to be
|
||||||
|
// paired with modifier keys (command / control) so we need
|
||||||
|
// to prevent the browser's regular actions (i.e. zooming
|
||||||
|
// the page). A user can zoom by unfocusing the editor.
|
||||||
if (e.metaKey || e.ctrlKey) {
|
if (e.metaKey || e.ctrlKey) {
|
||||||
preventDefault(e)
|
preventDefault(e)
|
||||||
return
|
return
|
||||||
|
@ -73,6 +67,9 @@ export function useDocumentEvents() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ',': {
|
case ',': {
|
||||||
|
// todo: extract to extension
|
||||||
|
// This seems very fragile; the comma key here is used to send pointer events,
|
||||||
|
// but that means it also needs to know about pen mode, hovered ids, etc.
|
||||||
if (!isFocusingInput()) {
|
if (!isFocusingInput()) {
|
||||||
preventDefault(e)
|
preventDefault(e)
|
||||||
if (!editor.inputs.keys.has('Comma')) {
|
if (!editor.inputs.keys.has('Comma')) {
|
||||||
|
|
|
@ -796,7 +796,7 @@ export function useMenuSchema(): TLUiMenuSchema;
|
||||||
export function useNativeClipboardEvents(): void;
|
export function useNativeClipboardEvents(): void;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useReadOnly(): boolean;
|
export function useReadonly(): boolean;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useToasts(): TLUiToastsContextType;
|
export function useToasts(): TLUiToastsContextType;
|
||||||
|
|
|
@ -83,7 +83,7 @@ export {
|
||||||
type TLUiMenuSchemaContextType,
|
type TLUiMenuSchemaContextType,
|
||||||
type TLUiMenuSchemaProviderProps,
|
type TLUiMenuSchemaProviderProps,
|
||||||
} from './lib/ui/hooks/useMenuSchema'
|
} from './lib/ui/hooks/useMenuSchema'
|
||||||
export { useReadOnly } from './lib/ui/hooks/useReadOnly'
|
export { useReadonly } from './lib/ui/hooks/useReadonly'
|
||||||
export {
|
export {
|
||||||
useToasts,
|
useToasts,
|
||||||
type TLUiToast,
|
type TLUiToast,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { useReadOnly } from '../ui/hooks/useReadOnly'
|
import { useReadonly } from '../ui/hooks/useReadonly'
|
||||||
import { CropHandles } from './CropHandles'
|
import { CropHandles } from './CropHandles'
|
||||||
|
|
||||||
const IS_FIREFOX =
|
const IS_FIREFOX =
|
||||||
|
@ -25,7 +25,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const rSvg = useRef<SVGSVGElement>(null)
|
const rSvg = useRef<SVGSVGElement>(null)
|
||||||
|
|
||||||
const isReadonlyMode = useReadOnly()
|
const isReadonlyMode = useReadonly()
|
||||||
const topEvents = useSelectionEvents('top')
|
const topEvents = useSelectionEvents('top')
|
||||||
const rightEvents = useSelectionEvents('right')
|
const rightEvents = useSelectionEvents('right')
|
||||||
const bottomEvents = useSelectionEvents('bottom')
|
const bottomEvents = useSelectionEvents('bottom')
|
||||||
|
|
|
@ -557,7 +557,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
'select.pointing_handle',
|
'select.pointing_handle',
|
||||||
'select.dragging_handle',
|
'select.dragging_handle',
|
||||||
'arrow.dragging'
|
'arrow.dragging'
|
||||||
) && !this.editor.instanceState.isReadOnly
|
) && !this.editor.instanceState.isReadonly
|
||||||
|
|
||||||
const info = this.editor.getArrowInfo(shape)
|
const info = this.editor.getArrowInfo(shape)
|
||||||
const bounds = this.editor.getBounds(shape)
|
const bounds = this.editor.getBounds(shape)
|
||||||
|
|
|
@ -87,7 +87,7 @@ export class Idle extends StateNode {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'handle': {
|
case 'handle': {
|
||||||
if (this.editor.instanceState.isReadOnly) break
|
if (this.editor.instanceState.isReadonly) break
|
||||||
if (this.editor.inputs.altKey) {
|
if (this.editor.inputs.altKey) {
|
||||||
this.parent.transition('pointing_shape', info)
|
this.parent.transition('pointing_shape', info)
|
||||||
} else {
|
} else {
|
||||||
|
@ -142,12 +142,12 @@ export class Idle extends StateNode {
|
||||||
switch (info.target) {
|
switch (info.target) {
|
||||||
case 'canvas': {
|
case 'canvas': {
|
||||||
// Create text shape and transition to editing_shape
|
// Create text shape and transition to editing_shape
|
||||||
if (this.editor.instanceState.isReadOnly) break
|
if (this.editor.instanceState.isReadonly) break
|
||||||
this.handleDoubleClickOnCanvas(info)
|
this.handleDoubleClickOnCanvas(info)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'selection': {
|
case 'selection': {
|
||||||
if (this.editor.instanceState.isReadOnly) break
|
if (this.editor.instanceState.isReadonly) break
|
||||||
|
|
||||||
const { onlySelectedShape } = this.editor
|
const { onlySelectedShape } = this.editor
|
||||||
if (onlySelectedShape) {
|
if (onlySelectedShape) {
|
||||||
|
@ -191,7 +191,7 @@ export class Idle extends StateNode {
|
||||||
if (
|
if (
|
||||||
shape.type !== 'video' &&
|
shape.type !== 'video' &&
|
||||||
shape.type !== 'embed' &&
|
shape.type !== 'embed' &&
|
||||||
this.editor.instanceState.isReadOnly
|
this.editor.instanceState.isReadonly
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -221,7 +221,7 @@ export class Idle extends StateNode {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'handle': {
|
case 'handle': {
|
||||||
if (this.editor.instanceState.isReadOnly) break
|
if (this.editor.instanceState.isReadonly) break
|
||||||
const { shape, handle } = info
|
const { shape, handle } = info
|
||||||
|
|
||||||
const util = this.editor.getShapeUtil(shape)
|
const util = this.editor.getShapeUtil(shape)
|
||||||
|
@ -307,7 +307,7 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onKeyUp = (info: TLKeyboardEventInfo) => {
|
override onKeyUp = (info: TLKeyboardEventInfo) => {
|
||||||
if (this.editor.instanceState.isReadOnly) {
|
if (this.editor.instanceState.isReadonly) {
|
||||||
switch (info.code) {
|
switch (info.code) {
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) {
|
if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export class PointingSelection extends StateNode {
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||||
if (this.editor.inputs.isDragging) {
|
if (this.editor.inputs.isDragging) {
|
||||||
if (this.editor.instanceState.isReadOnly) return
|
if (this.editor.instanceState.isReadonly) return
|
||||||
this.parent.transition('translating', info)
|
this.parent.transition('translating', info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,7 @@ export class PointingShape extends StateNode {
|
||||||
|
|
||||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||||
if (this.editor.inputs.isDragging) {
|
if (this.editor.inputs.isDragging) {
|
||||||
if (this.editor.instanceState.isReadOnly) return
|
if (this.editor.instanceState.isReadonly) return
|
||||||
this.parent.transition('translating', info)
|
this.parent.transition('translating', info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ToastProvider } from '@radix-ui/react-toast'
|
import { ToastProvider } from '@radix-ui/react-toast'
|
||||||
import { useEditor, useValue } from '@tldraw/editor'
|
import { preventDefault, useEditor, useValue } from '@tldraw/editor'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
|
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
|
||||||
|
@ -119,7 +119,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
const isReadonlyMode = useValue('isReadOnlyMode', () => editor.instanceState.isReadOnly, [editor])
|
const isReadonlyMode = useValue('isReadonlyMode', () => editor.instanceState.isReadonly, [editor])
|
||||||
const isFocusMode = useValue('focus', () => editor.instanceState.isFocusMode, [editor])
|
const isFocusMode = useValue('focus', () => editor.instanceState.isFocusMode, [editor])
|
||||||
const isDebugMode = useValue('debug', () => editor.instanceState.isDebugMode, [editor])
|
const isDebugMode = useValue('debug', () => editor.instanceState.isDebugMode, [editor])
|
||||||
|
|
||||||
|
@ -135,6 +135,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
||||||
className={classNames('tlui-layout', {
|
className={classNames('tlui-layout', {
|
||||||
'tlui-layout__mobile': breakpoint < 5,
|
'tlui-layout__mobile': breakpoint < 5,
|
||||||
})}
|
})}
|
||||||
|
onPointerDown={preventDefault}
|
||||||
>
|
>
|
||||||
{isFocusMode ? (
|
{isFocusMode ? (
|
||||||
<div className="tlui-layout__top">
|
<div className="tlui-layout__top">
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useContainer } from '@tldraw/editor'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||||
import { useActionsMenuSchema } from '../hooks/useActionsMenuSchema'
|
import { useActionsMenuSchema } from '../hooks/useActionsMenuSchema'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { Button } from './primitives/Button'
|
import { Button } from './primitives/Button'
|
||||||
import { Popover, PopoverTrigger } from './primitives/Popover'
|
import { Popover, PopoverTrigger } from './primitives/Popover'
|
||||||
|
@ -13,7 +13,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
const menuSchema = useActionsMenuSchema()
|
const menuSchema = useActionsMenuSchema()
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
function getActionMenuItem(item: TLUiMenuChild) {
|
function getActionMenuItem(item: TLUiMenuChild) {
|
||||||
if (isReadonly && !item.readonlyOk) return null
|
if (isReadonly && !item.readonlyOk) return null
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||||
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
|
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
|
||||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { MoveToPageMenu } from './MoveToPageMenu'
|
import { MoveToPageMenu } from './MoveToPageMenu'
|
||||||
import { Button } from './primitives/Button'
|
import { Button } from './primitives/Button'
|
||||||
|
@ -66,7 +66,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
|
||||||
const [_, handleOpenChange] = useMenuIsOpen('context menu', cb)
|
const [_, handleOpenChange] = useMenuIsOpen('context menu', cb)
|
||||||
|
|
||||||
// If every item in the menu is readonly, then we don't want to show the menu
|
// If every item in the menu is readonly, then we don't want to show the menu
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
const noItemsToShow =
|
const noItemsToShow =
|
||||||
contextTLUiMenuSchema.length === 0 ||
|
contextTLUiMenuSchema.length === 0 ||
|
||||||
|
@ -98,7 +98,7 @@ function ContextMenuContent() {
|
||||||
const menuSchema = useContextMenuSchema()
|
const menuSchema = useContextMenuSchema()
|
||||||
const [_, handleSubOpenChange] = useMenuIsOpen('context menu sub')
|
const [_, handleSubOpenChange] = useMenuIsOpen('context menu sub')
|
||||||
|
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
||||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||||
import { useHelpMenuSchema } from '../hooks/useHelpMenuSchema'
|
import { useHelpMenuSchema } from '../hooks/useHelpMenuSchema'
|
||||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { TLUiIconType } from '../icon-types'
|
import { TLUiIconType } from '../icon-types'
|
||||||
|
@ -59,7 +59,7 @@ export const HelpMenu = React.memo(function HelpMenu() {
|
||||||
function HelpMenuContent() {
|
function HelpMenuContent() {
|
||||||
const menuSchema = useHelpMenuSchema()
|
const menuSchema = useHelpMenuSchema()
|
||||||
|
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
function getHelpMenuItem(item: TLUiMenuChild) {
|
function getHelpMenuItem(item: TLUiMenuChild) {
|
||||||
if (isReadonly && !item.readonlyOk) return null
|
if (isReadonly && !item.readonlyOk) return null
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||||
import { useKeyboardShortcutsSchema } from '../hooks/useKeyboardShortcutsSchema'
|
import { useKeyboardShortcutsSchema } from '../hooks/useKeyboardShortcutsSchema'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import * as Dialog from './primitives/Dialog'
|
import * as Dialog from './primitives/Dialog'
|
||||||
|
@ -8,7 +8,7 @@ import { Kbd } from './primitives/Kbd'
|
||||||
|
|
||||||
export const KeyboardShortcutsDialog = () => {
|
export const KeyboardShortcutsDialog = () => {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
const shortcutsItems = useKeyboardShortcutsSchema()
|
const shortcutsItems = useKeyboardShortcutsSchema()
|
||||||
|
|
||||||
function getKeyboardShortcutItem(item: TLUiMenuChild) {
|
function getKeyboardShortcutItem(item: TLUiMenuChild) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react'
|
||||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||||
import { useMenuSchema } from '../hooks/useMenuSchema'
|
import { useMenuSchema } from '../hooks/useMenuSchema'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { LanguageMenu } from './LanguageMenu'
|
import { LanguageMenu } from './LanguageMenu'
|
||||||
import { Button } from './primitives/Button'
|
import { Button } from './primitives/Button'
|
||||||
|
@ -35,7 +35,7 @@ function MenuContent() {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const menuSchema = useMenuSchema()
|
const menuSchema = useMenuSchema()
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
function getMenuItem(
|
function getMenuItem(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { track, useEditor } from '@tldraw/editor'
|
import { track, useEditor } from '@tldraw/editor'
|
||||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { ActionsMenu } from './ActionsMenu'
|
import { ActionsMenu } from './ActionsMenu'
|
||||||
import { DuplicateButton } from './DuplicateButton'
|
import { DuplicateButton } from './DuplicateButton'
|
||||||
import { Menu } from './Menu'
|
import { Menu } from './Menu'
|
||||||
|
@ -13,7 +13,7 @@ export const MenuZone = track(function MenuZone() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const breakpoint = useBreakpoint()
|
const breakpoint = useBreakpoint()
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser')
|
const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser')
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
||||||
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
||||||
import { useReadOnly } from '../../hooks/useReadOnly'
|
import { useReadonly } from '../../hooks/useReadonly'
|
||||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
import { Button } from '../primitives/Button'
|
import { Button } from '../primitives/Button'
|
||||||
import { Icon } from '../primitives/Icon'
|
import { Icon } from '../primitives/Icon'
|
||||||
|
@ -36,7 +36,7 @@ export const PageMenu = function PageMenu() {
|
||||||
const currentPage = useValue('currentPage', () => editor.currentPage, [editor])
|
const currentPage = useValue('currentPage', () => editor.currentPage, [editor])
|
||||||
|
|
||||||
// When in readonly mode, we don't allow a user to edit the pages
|
// When in readonly mode, we don't allow a user to edit the pages
|
||||||
const isReadonlyMode = useReadOnly()
|
const isReadonlyMode = useReadonly()
|
||||||
|
|
||||||
// If the user has reached the max page count, we disable the "add page" button
|
// If the user has reached the max page count, we disable the "add page" button
|
||||||
const maxPageCountReached = useValue(
|
const maxPageCountReached = useValue(
|
||||||
|
|
|
@ -142,7 +142,11 @@ function CommonStylePickerSet({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tlui-style-panel__section__common" aria-label="style panel styles">
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
className="tlui-style-panel__section__common"
|
||||||
|
aria-label="style panel styles"
|
||||||
|
>
|
||||||
{color === undefined ? null : (
|
{color === undefined ? null : (
|
||||||
<ButtonPicker
|
<ButtonPicker
|
||||||
title={msg('style-panel.color')}
|
title={msg('style-panel.color')}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { GeoShapeGeoStyle, preventDefault, track, useEditor, useValue } from '@t
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
||||||
import { useReadOnly } from '../../hooks/useReadOnly'
|
import { useReadonly } from '../../hooks/useReadonly'
|
||||||
import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema'
|
import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema'
|
||||||
import { TLUiToolItem } from '../../hooks/useTools'
|
import { TLUiToolItem } from '../../hooks/useTools'
|
||||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
|
@ -25,7 +25,7 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
|
|
||||||
const rMostRecentlyActiveDropdownItem = React.useRef<TLUiToolbarItem | undefined>(undefined)
|
const rMostRecentlyActiveDropdownItem = React.useRef<TLUiToolbarItem | undefined>(undefined)
|
||||||
|
|
||||||
const isReadOnly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
const toolbarItems = useToolbarSchema()
|
const toolbarItems = useToolbarSchema()
|
||||||
const laserTool = toolbarItems.find((item) => item.toolItem.id === 'laser')
|
const laserTool = toolbarItems.find((item) => item.toolItem.id === 'laser')
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
editor,
|
editor,
|
||||||
])
|
])
|
||||||
|
|
||||||
const showEditingTools = !isReadOnly
|
const showEditingTools = !isReadonly
|
||||||
const showExtraActions = !(isReadOnly || isHandTool)
|
const showExtraActions = !(isReadonly || isHandTool)
|
||||||
|
|
||||||
const getTitle = (item: TLUiToolItem) =>
|
const getTitle = (item: TLUiToolItem) =>
|
||||||
item.label ? `${msg(item.label)} ${item.kbd ? kbdStr(item.kbd) : ''}` : ''
|
item.label ? `${msg(item.label)} ${item.kbd ? kbdStr(item.kbd) : ''}` : ''
|
||||||
|
@ -110,7 +110,7 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
<div className="tlui-toolbar">
|
<div className="tlui-toolbar">
|
||||||
<div className="tlui-toolbar__inner">
|
<div className="tlui-toolbar__inner">
|
||||||
<div className="tlui-toolbar__left">
|
<div className="tlui-toolbar__left">
|
||||||
{!isReadOnly && (
|
{!isReadonly && (
|
||||||
<div
|
<div
|
||||||
className={classNames('tlui-toolbar__extras', {
|
className={classNames('tlui-toolbar__extras', {
|
||||||
'tlui-toolbar__extras__hidden': !showExtraActions,
|
'tlui-toolbar__extras__hidden': !showExtraActions,
|
||||||
|
@ -144,7 +144,7 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{isReadOnly && laserTool && (
|
{isReadonly && laserTool && (
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
key={laserTool.toolItem.id}
|
key={laserTool.toolItem.id}
|
||||||
item={laserTool.toolItem}
|
item={laserTool.toolItem}
|
||||||
|
@ -208,7 +208,7 @@ export const Toolbar = memo(function Toolbar() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{breakpoint < 5 && !isReadOnly && (
|
{breakpoint < 5 && !isReadonly && (
|
||||||
<div className="tlui-toolbar__tools">
|
<div className="tlui-toolbar__tools">
|
||||||
<MobileStylePanel />
|
<MobileStylePanel />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { track, useEditor } from '@tldraw/editor'
|
import { track, useEditor } from '@tldraw/editor'
|
||||||
import { useActions } from '../hooks/useActions'
|
import { useActions } from '../hooks/useActions'
|
||||||
import { useReadOnly } from '../hooks/useReadOnly'
|
import { useReadonly } from '../hooks/useReadonly'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
import { Button } from './primitives/Button'
|
import { Button } from './primitives/Button'
|
||||||
import { kbdStr } from './primitives/shared'
|
import { kbdStr } from './primitives/shared'
|
||||||
|
@ -11,7 +11,7 @@ export const TrashButton = track(function TrashButton() {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const action = actions['delete']
|
const action = actions['delete']
|
||||||
|
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
|
|
||||||
if (isReadonly) return null
|
if (isReadonly) return null
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { preventDefault, useEditor } from '@tldraw/editor'
|
import { preventDefault, useEditor, useValue } from '@tldraw/editor'
|
||||||
import hotkeys from 'hotkeys-js'
|
import hotkeys from 'hotkeys-js'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useActions } from './useActions'
|
import { useActions } from './useActions'
|
||||||
import { useReadOnly } from './useReadOnly'
|
import { useReadonly } from './useReadonly'
|
||||||
import { useTools } from './useTools'
|
import { useTools } from './useTools'
|
||||||
|
|
||||||
const SKIP_KBDS = [
|
const SKIP_KBDS = [
|
||||||
|
@ -18,11 +18,14 @@ const SKIP_KBDS = [
|
||||||
export function useKeyboardShortcuts() {
|
export function useKeyboardShortcuts() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const isReadonly = useReadOnly()
|
const isReadonly = useReadonly()
|
||||||
const actions = useActions()
|
const actions = useActions()
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
|
const isFocused = useValue('is focused', () => editor.instanceState.isFocused, [editor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isFocused) return
|
||||||
|
|
||||||
const container = editor.getContainer()
|
const container = editor.getContainer()
|
||||||
|
|
||||||
hotkeys.setScope(editor.store.id)
|
hotkeys.setScope(editor.store.id)
|
||||||
|
@ -34,9 +37,7 @@ export function useKeyboardShortcuts() {
|
||||||
// Add hotkeys for actions and tools.
|
// Add hotkeys for actions and tools.
|
||||||
// Except those that in SKIP_KBDS!
|
// Except those that in SKIP_KBDS!
|
||||||
const areShortcutsDisabled = () =>
|
const areShortcutsDisabled = () =>
|
||||||
(editor.instanceState.isFocused && editor.isMenuOpen) ||
|
editor.isMenuOpen || editor.editingId !== null || editor.crashingError
|
||||||
editor.editingId !== null ||
|
|
||||||
editor.crashingError
|
|
||||||
|
|
||||||
for (const action of Object.values(actions)) {
|
for (const action of Object.values(actions)) {
|
||||||
if (!action.kbd) continue
|
if (!action.kbd) continue
|
||||||
|
@ -51,7 +52,9 @@ export function useKeyboardShortcuts() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tool of Object.values(tools)) {
|
for (const tool of Object.values(tools)) {
|
||||||
if (!tool.kbd || (!tool.readonlyOk && editor.instanceState.isReadOnly)) continue
|
if (!tool.kbd || (!tool.readonlyOk && editor.instanceState.isReadonly)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (SKIP_KBDS.includes(tool.id)) continue
|
if (SKIP_KBDS.includes(tool.id)) continue
|
||||||
|
|
||||||
|
@ -65,7 +68,7 @@ export function useKeyboardShortcuts() {
|
||||||
return () => {
|
return () => {
|
||||||
hotkeys.deleteScope(editor.store.id)
|
hotkeys.deleteScope(editor.store.id)
|
||||||
}
|
}
|
||||||
}, [actions, tools, isReadonly, editor])
|
}, [actions, tools, isReadonly, editor, isFocused])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHotkeysStringFromKbd(kbd: string) {
|
function getHotkeysStringFromKbd(kbd: string) {
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { useEditor, useValue } from '@tldraw/editor'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function useReadOnly() {
|
|
||||||
const editor = useEditor()
|
|
||||||
return useValue('isReadOnlyMode', () => editor.instanceState.isReadOnly, [editor])
|
|
||||||
}
|
|
7
packages/tldraw/src/lib/ui/hooks/useReadonly.ts
Normal file
7
packages/tldraw/src/lib/ui/hooks/useReadonly.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { useEditor, useValue } from '@tldraw/editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function useReadonly() {
|
||||||
|
const editor = useEditor()
|
||||||
|
return useValue('isReadonlyMode', () => editor.instanceState.isReadonly, [editor])
|
||||||
|
}
|
|
@ -285,7 +285,7 @@ describe("App's default tool", () => {
|
||||||
})
|
})
|
||||||
it('Is hand for readonly mode', () => {
|
it('Is hand for readonly mode', () => {
|
||||||
editor = new TestEditor()
|
editor = new TestEditor()
|
||||||
editor.updateInstanceState({ isReadOnly: true })
|
editor.updateInstanceState({ isReadonly: true })
|
||||||
editor.setCurrentTool('hand')
|
editor.setCurrentTool('hand')
|
||||||
expect(editor.currentToolId).toBe('hand')
|
expect(editor.currentToolId).toBe('hand')
|
||||||
})
|
})
|
||||||
|
@ -369,37 +369,42 @@ describe('isFocused', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('becomes true when the container div receives a focus event', () => {
|
it('becomes true when the container div receives a focus event', () => {
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(false)
|
expect(editor.instanceState.isFocused).toBe(false)
|
||||||
|
|
||||||
editor.elm.focus()
|
editor.elm.focus()
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(true)
|
expect(editor.instanceState.isFocused).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('becomes false when the container div receives a blur event', () => {
|
it('becomes false when the container div receives a blur event', () => {
|
||||||
editor.elm.focus()
|
editor.elm.focus()
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(true)
|
expect(editor.instanceState.isFocused).toBe(true)
|
||||||
|
|
||||||
editor.elm.blur()
|
editor.elm.blur()
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(false)
|
expect(editor.instanceState.isFocused).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('becomes true when a child of the app container div receives a focusin event', () => {
|
it.skip('becomes true when a child of the app container div receives a focusin event', () => {
|
||||||
|
// We need to skip this one because it's not actually true: the focusin event will bubble
|
||||||
|
// to the document.body, resulting in that being the active element. In reality, the editor's
|
||||||
|
// container would also have received a focus event, and after the editor's debounce ends,
|
||||||
|
// the container (or one of its descendants) will be the focused element.
|
||||||
editor.elm.blur()
|
editor.elm.blur()
|
||||||
|
|
||||||
const child = document.createElement('div')
|
const child = document.createElement('div')
|
||||||
editor.elm.appendChild(child)
|
editor.elm.appendChild(child)
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(false)
|
expect(editor.instanceState.isFocused).toBe(false)
|
||||||
|
|
||||||
child.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
|
child.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(true)
|
expect(editor.instanceState.isFocused).toBe(true)
|
||||||
|
|
||||||
child.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
child.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(false)
|
expect(editor.instanceState.isFocused).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -413,6 +418,7 @@ describe('isFocused', () => {
|
||||||
|
|
||||||
child.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
child.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(100)
|
||||||
expect(editor.instanceState.isFocused).toBe(false)
|
expect(editor.instanceState.isFocused).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -412,7 +412,7 @@ describe('When in readonly mode', () => {
|
||||||
props: { w: 100, h: 100, url: '' },
|
props: { w: 100, h: 100, url: '' },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
editor.updateInstanceState({ isReadOnly: true })
|
editor.updateInstanceState({ isReadonly: true })
|
||||||
editor.setCurrentTool('hand')
|
editor.setCurrentTool('hand')
|
||||||
editor.setCurrentTool('select')
|
editor.setCurrentTool('select')
|
||||||
})
|
})
|
||||||
|
@ -420,7 +420,7 @@ describe('When in readonly mode', () => {
|
||||||
it('Begins editing embed when double clicked', () => {
|
it('Begins editing embed when double clicked', () => {
|
||||||
expect(editor.editingId).toBe(null)
|
expect(editor.editingId).toBe(null)
|
||||||
expect(editor.selectedIds.length).toBe(0)
|
expect(editor.selectedIds.length).toBe(0)
|
||||||
expect(editor.instanceState.isReadOnly).toBe(true)
|
expect(editor.instanceState.isReadonly).toBe(true)
|
||||||
|
|
||||||
const shape = editor.getShapeById(ids.embed1)
|
const shape = editor.getShapeById(ids.embed1)
|
||||||
editor.doubleClick(100, 100, { target: 'shape', shape })
|
editor.doubleClick(100, 100, { target: 'shape', shape })
|
||||||
|
@ -430,7 +430,7 @@ describe('When in readonly mode', () => {
|
||||||
it('Begins editing embed when pressing Enter on a selected embed', () => {
|
it('Begins editing embed when pressing Enter on a selected embed', () => {
|
||||||
expect(editor.editingId).toBe(null)
|
expect(editor.editingId).toBe(null)
|
||||||
expect(editor.selectedIds.length).toBe(0)
|
expect(editor.selectedIds.length).toBe(0)
|
||||||
expect(editor.instanceState.isReadOnly).toBe(true)
|
expect(editor.instanceState.isReadonly).toBe(true)
|
||||||
|
|
||||||
editor.setSelectedIds([ids.embed1])
|
editor.setSelectedIds([ids.embed1])
|
||||||
expect(editor.selectedIds.length).toBe(1)
|
expect(editor.selectedIds.length).toBe(1)
|
||||||
|
|
|
@ -279,7 +279,7 @@ describe('creating groups', () => {
|
||||||
// │ A │ │ B │ │ C │
|
// │ A │ │ B │ │ C │
|
||||||
// └───┘ └───┘ └───┘
|
// └───┘ └───┘ └───┘
|
||||||
editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
|
editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)])
|
||||||
editor.updateInstanceState({ isReadOnly: true })
|
editor.updateInstanceState({ isReadonly: true })
|
||||||
editor.setCurrentTool('hand')
|
editor.setCurrentTool('hand')
|
||||||
editor.selectAll()
|
editor.selectAll()
|
||||||
expect(editor.selectedIds.length).toBe(3)
|
expect(editor.selectedIds.length).toBe(3)
|
||||||
|
@ -491,7 +491,7 @@ describe('ungrouping shapes', () => {
|
||||||
expect(editor.selectedIds.length).toBe(3)
|
expect(editor.selectedIds.length).toBe(3)
|
||||||
editor.groupShapes()
|
editor.groupShapes()
|
||||||
expect(editor.selectedIds.length).toBe(1)
|
expect(editor.selectedIds.length).toBe(1)
|
||||||
editor.updateInstanceState({ isReadOnly: true })
|
editor.updateInstanceState({ isReadonly: true })
|
||||||
editor.setCurrentTool('hand')
|
editor.setCurrentTool('hand')
|
||||||
|
|
||||||
editor.ungroupShapes()
|
editor.ungroupShapes()
|
||||||
|
|
|
@ -1020,7 +1020,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isPenMode: boolean;
|
isPenMode: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isReadOnly: boolean;
|
isReadonly: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isToolLocked: boolean;
|
isToolLocked: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -1341,6 +1341,22 @@ describe('adds lonely properties', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('rename isReadOnly to isReadonly', () => {
|
||||||
|
const { up, down } = instanceMigrations.migrators[instanceVersions.ReadOnlyReadonly]
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ isReadOnly: false })).toStrictEqual({
|
||||||
|
isReadonly: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ isReadonly: false })).toStrictEqual({
|
||||||
|
isReadOnly: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||||
|
|
||||||
for (const migrator of allMigrators) {
|
for (const migrator of allMigrators) {
|
||||||
|
|
|
@ -42,7 +42,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
isCoarsePointer: boolean
|
isCoarsePointer: boolean
|
||||||
openMenus: string[]
|
openMenus: string[]
|
||||||
isChangingStyle: boolean
|
isChangingStyle: boolean
|
||||||
isReadOnly: boolean
|
isReadonly: boolean
|
||||||
meta: JsonObject
|
meta: JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
|
||||||
isCoarsePointer: T.boolean,
|
isCoarsePointer: T.boolean,
|
||||||
openMenus: T.arrayOf(T.string),
|
openMenus: T.arrayOf(T.string),
|
||||||
isChangingStyle: T.boolean,
|
isChangingStyle: T.boolean,
|
||||||
isReadOnly: T.boolean,
|
isReadonly: T.boolean,
|
||||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -124,7 +124,7 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
|
||||||
isCoarsePointer: false,
|
isCoarsePointer: false,
|
||||||
openMenus: [] as string[],
|
openMenus: [] as string[],
|
||||||
isChangingStyle: false,
|
isChangingStyle: false,
|
||||||
isReadOnly: false,
|
isReadonly: false,
|
||||||
meta: {},
|
meta: {},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -151,11 +151,12 @@ export const instanceVersions = {
|
||||||
AddMeta: 17,
|
AddMeta: 17,
|
||||||
RemoveCursorColor: 18,
|
RemoveCursorColor: 18,
|
||||||
AddLonelyProperties: 19,
|
AddLonelyProperties: 19,
|
||||||
|
ReadOnlyReadonly: 20,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const instanceMigrations = defineMigrations({
|
export const instanceMigrations = defineMigrations({
|
||||||
currentVersion: instanceVersions.AddLonelyProperties,
|
currentVersion: instanceVersions.ReadOnlyReadonly,
|
||||||
migrators: {
|
migrators: {
|
||||||
[instanceVersions.AddTransparentExportBgs]: {
|
[instanceVersions.AddTransparentExportBgs]: {
|
||||||
up: (instance: TLInstance) => {
|
up: (instance: TLInstance) => {
|
||||||
|
@ -438,6 +439,20 @@ export const instanceMigrations = defineMigrations({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[instanceVersions.ReadOnlyReadonly]: {
|
||||||
|
up: ({ isReadOnly: _isReadOnly, ...record }) => {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
isReadonly: _isReadOnly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: ({ isReadonly: _isReadonly, ...record }) => {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
isReadOnly: _isReadonly,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue