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>
|
||||
<p>This is the second example.</p>
|
||||
<div style={{ width: '100%', height: '600px', padding: 32 }} tabIndex={-1}>
|
||||
<Tldraw persistenceKey="steve" autoFocus />
|
||||
<Tldraw persistenceKey="steve" className="A" autoFocus />
|
||||
</div>
|
||||
|
||||
<textarea defaultValue="type in me" style={{ margin: 10 }}></textarea>
|
||||
|
@ -20,7 +20,7 @@ export default function MultipleExample() {
|
|||
<h2>Second Example</h2>
|
||||
<p>This is the second example.</p>
|
||||
<div style={{ width: '100%', height: '600px' }} tabIndex={-1}>
|
||||
<Tldraw persistenceKey="david" autoFocus={false} />
|
||||
<Tldraw persistenceKey="david" className="B" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
"page-menu.submenu.duplicate-page": "Duplicar",
|
||||
"page-menu.submenu.delete": "Deletar",
|
||||
"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",
|
||||
"edit-link-dialog.cancel": "Cancelar",
|
||||
"embed-dialog.cancel": "Cancelar",
|
||||
|
|
|
@ -1643,6 +1643,7 @@ export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
|
|||
export interface TldrawEditorBaseProps {
|
||||
autoFocus?: boolean;
|
||||
children?: any;
|
||||
className?: string;
|
||||
components?: Partial<TLEditorComponents>;
|
||||
initialState?: string;
|
||||
onMount?: TLOnMountHandler;
|
||||
|
|
|
@ -191,6 +191,10 @@
|
|||
-webkit-touch-callout: initial;
|
||||
}
|
||||
|
||||
.tl-container:focus-within {
|
||||
outline: 1px solid var(--color-low);
|
||||
}
|
||||
|
||||
input,
|
||||
*[contenteditable],
|
||||
*[contenteditable] * {
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, {
|
|||
useSyncExternalStore,
|
||||
} from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { Canvas } from './components/Canvas'
|
||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||
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).
|
||||
*/
|
||||
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({
|
||||
store,
|
||||
components,
|
||||
className,
|
||||
...rest
|
||||
}: TldrawEditorProps) {
|
||||
const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
|
||||
const [container, rContainer] = React.useState<HTMLDivElement | null>(null)
|
||||
const user = useMemo(() => createTLUser(), [])
|
||||
|
||||
const ErrorFallback =
|
||||
|
@ -137,7 +144,12 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|||
}
|
||||
|
||||
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
|
||||
fallback={ErrorFallback}
|
||||
onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })}
|
||||
|
|
|
@ -188,9 +188,9 @@ function HandlesWrapper() {
|
|||
const isChangingStyle = useValue('isChangingStyle', () => editor.instanceState.isChangingStyle, [
|
||||
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)
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
annotateError,
|
||||
assert,
|
||||
compact,
|
||||
debounce,
|
||||
dedupe,
|
||||
deepCopy,
|
||||
getOwnProperty,
|
||||
|
@ -306,19 +307,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const container = this.getContainer()
|
||||
|
||||
const handleFocus = () => this.updateInstanceState({ isFocused: true })
|
||||
const handleBlur = () => this.updateInstanceState({ isFocused: false })
|
||||
// We need to debounce this because when focus changes, the body
|
||||
// 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('focus', handleFocus)
|
||||
container.addEventListener('focusout', handleBlur)
|
||||
container.addEventListener('blur', handleBlur)
|
||||
container.addEventListener('focusin', updateFocus)
|
||||
container.addEventListener('focus', updateFocus)
|
||||
container.addEventListener('focusout', updateFocus)
|
||||
container.addEventListener('blur', updateFocus)
|
||||
|
||||
this.disposables.add(() => {
|
||||
container.removeEventListener('focusin', handleFocus)
|
||||
container.removeEventListener('focus', handleFocus)
|
||||
container.removeEventListener('focusout', handleBlur)
|
||||
container.removeEventListener('blur', handleBlur)
|
||||
container.removeEventListener('focusin', updateFocus)
|
||||
container.removeEventListener('focus', updateFocus)
|
||||
container.removeEventListener('focusout', updateFocus)
|
||||
container.removeEventListener('blur', updateFocus)
|
||||
})
|
||||
|
||||
this.store.ensureStoreIsUsable()
|
||||
|
@ -1046,26 +1060,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
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.
|
||||
*
|
||||
|
@ -1084,6 +1078,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
this.root.transition(id, info)
|
||||
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.
|
||||
|
@ -3276,7 +3289,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _updatePage = this.history.createCommand(
|
||||
'updatePage',
|
||||
(partial: RequiredKeys<TLPage, 'id'>, squashing = false) => {
|
||||
if (this.instanceState.isReadOnly) return null
|
||||
if (this.instanceState.isReadonly) return null
|
||||
|
||||
const prev = this.getPageById(partial.id)
|
||||
|
||||
|
@ -3326,7 +3339,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _createPage = this.history.createCommand(
|
||||
'createPage',
|
||||
(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
|
||||
const pageInfo = this.pages
|
||||
const topIndex = belowPageIndex ?? pageInfo[pageInfo.length - 1]?.index ?? 'a1'
|
||||
|
@ -3408,7 +3421,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _deletePage = this.history.createCommand(
|
||||
'delete_page',
|
||||
(id: TLPageId) => {
|
||||
if (this.instanceState.isReadOnly) return null
|
||||
if (this.instanceState.isReadonly) return null
|
||||
const { pages } = this
|
||||
if (pages.length === 1) return null
|
||||
|
||||
|
@ -3492,7 +3505,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
renamePage(id: TLPageId, name: string, squashing = false) {
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
this.updatePage({ id, name }, squashing)
|
||||
return this
|
||||
}
|
||||
|
@ -3533,7 +3546,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _createAssets = this.history.createCommand(
|
||||
'createAssets',
|
||||
(assets: TLAsset[]) => {
|
||||
if (this.instanceState.isReadOnly) return null
|
||||
if (this.instanceState.isReadonly) return null
|
||||
if (assets.length <= 0) return null
|
||||
|
||||
return { data: { assets } }
|
||||
|
@ -3569,7 +3582,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _updateAssets = this.history.createCommand(
|
||||
'updateAssets',
|
||||
(assets: TLAssetPartial[]) => {
|
||||
if (this.instanceState.isReadOnly) return
|
||||
if (this.instanceState.isReadonly) return
|
||||
if (assets.length <= 0) return
|
||||
|
||||
const snapshots: Record<string, TLAsset> = {}
|
||||
|
@ -3616,7 +3629,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _deleteAssets = this.history.createCommand(
|
||||
'deleteAssets',
|
||||
(ids: TLAssetId[]) => {
|
||||
if (this.instanceState.isReadOnly) return
|
||||
if (this.instanceState.isReadonly) return
|
||||
if (ids.length <= 0) return
|
||||
|
||||
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 {
|
||||
if (ids.length === 0) return this
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
|
||||
const { currentPageId } = this
|
||||
|
||||
|
@ -5293,7 +5306,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
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,
|
||||
allUnlocked = true
|
||||
|
@ -5414,7 +5427,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
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)))
|
||||
|
||||
|
@ -5478,7 +5491,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
ids: TLShapeId[] = this.currentPageState.selectedIds,
|
||||
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) => {
|
||||
if (!shape) return false
|
||||
|
@ -5605,7 +5618,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @param padding - The padding to apply to the packed shapes.
|
||||
*/
|
||||
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
|
||||
|
||||
const shapes = compact(
|
||||
|
@ -5764,7 +5777,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom',
|
||||
ids: TLShapeId[] = this.currentPageState.selectedIds
|
||||
) {
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
if (ids.length < 2) return this
|
||||
|
||||
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
||||
|
@ -5851,7 +5864,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
operation: 'horizontal' | 'vertical',
|
||||
ids: TLShapeId[] = this.currentPageState.selectedIds
|
||||
) {
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
if (ids.length < 3) return this
|
||||
|
||||
const len = ids.length
|
||||
|
@ -5936,7 +5949,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
operation: 'horizontal' | 'vertical',
|
||||
ids: TLShapeId[] = this.currentPageState.selectedIds
|
||||
) {
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
if (ids.length < 2) return this
|
||||
|
||||
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
||||
|
@ -6024,7 +6037,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
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.y)) scale = new Vec2d(scale.x, 1)
|
||||
|
@ -6296,7 +6309,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _createShapes = this.history.createCommand(
|
||||
'createShapes',
|
||||
(partials: TLShapePartial[], select = false) => {
|
||||
if (this.instanceState.isReadOnly) return null
|
||||
if (this.instanceState.isReadonly) return null
|
||||
if (partials.length <= 0) return null
|
||||
|
||||
const { currentPageShapeIds: shapeIds, selectedIds } = this
|
||||
|
@ -6584,7 +6597,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
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
|
||||
|
||||
|
@ -6639,7 +6652,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
ungroupShapes(ids: TLShapeId[] = this.selectedIds) {
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
if (ids.length === 0) return this
|
||||
|
||||
// Only ungroup when the select tool is active
|
||||
|
@ -6730,7 +6743,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _updateShapes = this.history.createCommand(
|
||||
'updateShapes',
|
||||
(_partials: (TLShapePartial | null | undefined)[], squashing = false) => {
|
||||
if (this.instanceState.isReadOnly) return null
|
||||
if (this.instanceState.isReadonly) return null
|
||||
|
||||
const partials = compact(_partials)
|
||||
|
||||
|
@ -6853,7 +6866,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
private _deleteShapes = this.history.createCommand(
|
||||
'delete_shapes',
|
||||
(ids: TLShapeId[]) => {
|
||||
if (this.instanceState.isReadOnly) return null
|
||||
if (this.instanceState.isReadonly) return null
|
||||
if (ids.length === 0) return null
|
||||
const prevSelectedIds = [...this.currentPageState.selectedIds]
|
||||
|
||||
|
@ -7444,7 +7457,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
preserveIds?: boolean
|
||||
} = {}
|
||||
): this {
|
||||
if (this.instanceState.isReadOnly) return this
|
||||
if (this.instanceState.isReadonly) return this
|
||||
|
||||
if (!content.schema) {
|
||||
throw Error('Could not put content:\ncontent is missing a schema.')
|
||||
|
|
|
@ -233,15 +233,17 @@ export class SnapManager {
|
|||
constructor(public readonly editor: Editor) {}
|
||||
|
||||
@computed get snapPointsCache() {
|
||||
return this.editor.store.createComputedCache<SnapPoint[], TLShape>('snapPoints', (shape) => {
|
||||
const pageTransfrorm = this.editor.getPageTransformById(shape.id)
|
||||
const { editor } = this
|
||||
return editor.store.createComputedCache<SnapPoint[], TLShape>('snapPoints', (shape) => {
|
||||
const pageTransfrorm = editor.getPageTransformById(shape.id)
|
||||
if (!pageTransfrorm) return undefined
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
const snapPoints = util.snapPoints(shape)
|
||||
return snapPoints.map((point, i) => {
|
||||
const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point)
|
||||
return { x, y, id: `${shape.id}:${i}` }
|
||||
})
|
||||
return editor
|
||||
.getShapeUtil(shape)
|
||||
.snapPoints(shape)
|
||||
.map((point, 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) {
|
||||
;(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)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export function useDocumentEvents() {
|
|||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (
|
||||
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')) &&
|
||||
!isFocusingInput()
|
||||
) {
|
||||
|
@ -45,21 +46,14 @@ export function useDocumentEvents() {
|
|||
;(e as any).isKilled = true
|
||||
|
||||
switch (e.key) {
|
||||
case '=': {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
preventDefault(e)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
case '-': {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
preventDefault(e)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
case '=':
|
||||
case '-':
|
||||
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) {
|
||||
preventDefault(e)
|
||||
return
|
||||
|
@ -73,6 +67,9 @@ export function useDocumentEvents() {
|
|||
break
|
||||
}
|
||||
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()) {
|
||||
preventDefault(e)
|
||||
if (!editor.inputs.keys.has('Comma')) {
|
||||
|
|
|
@ -796,7 +796,7 @@ export function useMenuSchema(): TLUiMenuSchema;
|
|||
export function useNativeClipboardEvents(): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useReadOnly(): boolean;
|
||||
export function useReadonly(): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useToasts(): TLUiToastsContextType;
|
||||
|
|
|
@ -83,7 +83,7 @@ export {
|
|||
type TLUiMenuSchemaContextType,
|
||||
type TLUiMenuSchemaProviderProps,
|
||||
} from './lib/ui/hooks/useMenuSchema'
|
||||
export { useReadOnly } from './lib/ui/hooks/useReadOnly'
|
||||
export { useReadonly } from './lib/ui/hooks/useReadonly'
|
||||
export {
|
||||
useToasts,
|
||||
type TLUiToast,
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@tldraw/editor'
|
||||
import classNames from 'classnames'
|
||||
import { useRef } from 'react'
|
||||
import { useReadOnly } from '../ui/hooks/useReadOnly'
|
||||
import { useReadonly } from '../ui/hooks/useReadonly'
|
||||
import { CropHandles } from './CropHandles'
|
||||
|
||||
const IS_FIREFOX =
|
||||
|
@ -25,7 +25,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
|
|||
const editor = useEditor()
|
||||
const rSvg = useRef<SVGSVGElement>(null)
|
||||
|
||||
const isReadonlyMode = useReadOnly()
|
||||
const isReadonlyMode = useReadonly()
|
||||
const topEvents = useSelectionEvents('top')
|
||||
const rightEvents = useSelectionEvents('right')
|
||||
const bottomEvents = useSelectionEvents('bottom')
|
||||
|
|
|
@ -557,7 +557,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
'select.pointing_handle',
|
||||
'select.dragging_handle',
|
||||
'arrow.dragging'
|
||||
) && !this.editor.instanceState.isReadOnly
|
||||
) && !this.editor.instanceState.isReadonly
|
||||
|
||||
const info = this.editor.getArrowInfo(shape)
|
||||
const bounds = this.editor.getBounds(shape)
|
||||
|
|
|
@ -87,7 +87,7 @@ export class Idle extends StateNode {
|
|||
break
|
||||
}
|
||||
case 'handle': {
|
||||
if (this.editor.instanceState.isReadOnly) break
|
||||
if (this.editor.instanceState.isReadonly) break
|
||||
if (this.editor.inputs.altKey) {
|
||||
this.parent.transition('pointing_shape', info)
|
||||
} else {
|
||||
|
@ -142,12 +142,12 @@ export class Idle extends StateNode {
|
|||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
// Create text shape and transition to editing_shape
|
||||
if (this.editor.instanceState.isReadOnly) break
|
||||
if (this.editor.instanceState.isReadonly) break
|
||||
this.handleDoubleClickOnCanvas(info)
|
||||
break
|
||||
}
|
||||
case 'selection': {
|
||||
if (this.editor.instanceState.isReadOnly) break
|
||||
if (this.editor.instanceState.isReadonly) break
|
||||
|
||||
const { onlySelectedShape } = this.editor
|
||||
if (onlySelectedShape) {
|
||||
|
@ -191,7 +191,7 @@ export class Idle extends StateNode {
|
|||
if (
|
||||
shape.type !== 'video' &&
|
||||
shape.type !== 'embed' &&
|
||||
this.editor.instanceState.isReadOnly
|
||||
this.editor.instanceState.isReadonly
|
||||
)
|
||||
break
|
||||
|
||||
|
@ -221,7 +221,7 @@ export class Idle extends StateNode {
|
|||
break
|
||||
}
|
||||
case 'handle': {
|
||||
if (this.editor.instanceState.isReadOnly) break
|
||||
if (this.editor.instanceState.isReadonly) break
|
||||
const { shape, handle } = info
|
||||
|
||||
const util = this.editor.getShapeUtil(shape)
|
||||
|
@ -307,7 +307,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onKeyUp = (info: TLKeyboardEventInfo) => {
|
||||
if (this.editor.instanceState.isReadOnly) {
|
||||
if (this.editor.instanceState.isReadonly) {
|
||||
switch (info.code) {
|
||||
case 'Enter': {
|
||||
if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) {
|
||||
|
|
|
@ -18,7 +18,7 @@ export class PointingSelection extends StateNode {
|
|||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
if (this.editor.instanceState.isReadOnly) return
|
||||
if (this.editor.instanceState.isReadonly) return
|
||||
this.parent.transition('translating', info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ export class PointingShape extends StateNode {
|
|||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
if (this.editor.instanceState.isReadOnly) return
|
||||
if (this.editor.instanceState.isReadonly) return
|
||||
this.parent.transition('translating', info)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 React, { ReactNode } from 'react'
|
||||
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
|
||||
|
@ -119,7 +119,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
const editor = useEditor()
|
||||
const msg = useTranslation()
|
||||
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 isDebugMode = useValue('debug', () => editor.instanceState.isDebugMode, [editor])
|
||||
|
||||
|
@ -135,6 +135,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
className={classNames('tlui-layout', {
|
||||
'tlui-layout__mobile': breakpoint < 5,
|
||||
})}
|
||||
onPointerDown={preventDefault}
|
||||
>
|
||||
{isFocusMode ? (
|
||||
<div className="tlui-layout__top">
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useContainer } from '@tldraw/editor'
|
|||
import { memo } from 'react'
|
||||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useActionsMenuSchema } from '../hooks/useActionsMenuSchema'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import { Popover, PopoverTrigger } from './primitives/Popover'
|
||||
|
@ -13,7 +13,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
|
|||
const msg = useTranslation()
|
||||
const container = useContainer()
|
||||
const menuSchema = useActionsMenuSchema()
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
function getActionMenuItem(item: TLUiMenuChild) {
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
|
|
@ -6,7 +6,7 @@ import { TLUiMenuChild } from '../hooks/menuHelpers'
|
|||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
|
||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { MoveToPageMenu } from './MoveToPageMenu'
|
||||
import { Button } from './primitives/Button'
|
||||
|
@ -66,7 +66,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
|
|||
const [_, handleOpenChange] = useMenuIsOpen('context menu', cb)
|
||||
|
||||
// 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 =
|
||||
contextTLUiMenuSchema.length === 0 ||
|
||||
|
@ -98,7 +98,7 @@ function ContextMenuContent() {
|
|||
const menuSchema = useContextMenuSchema()
|
||||
const [_, handleSubOpenChange] = useMenuIsOpen('context menu sub')
|
||||
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
const breakpoint = useBreakpoint()
|
||||
const container = useContainer()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react'
|
|||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useHelpMenuSchema } from '../hooks/useHelpMenuSchema'
|
||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { TLUiIconType } from '../icon-types'
|
||||
|
@ -59,7 +59,7 @@ export const HelpMenu = React.memo(function HelpMenu() {
|
|||
function HelpMenuContent() {
|
||||
const menuSchema = useHelpMenuSchema()
|
||||
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
function getHelpMenuItem(item: TLUiMenuChild) {
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useKeyboardShortcutsSchema } from '../hooks/useKeyboardShortcutsSchema'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import * as Dialog from './primitives/Dialog'
|
||||
|
@ -8,7 +8,7 @@ import { Kbd } from './primitives/Kbd'
|
|||
|
||||
export const KeyboardShortcutsDialog = () => {
|
||||
const msg = useTranslation()
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
const shortcutsItems = useKeyboardShortcutsSchema()
|
||||
|
||||
function getKeyboardShortcutItem(item: TLUiMenuChild) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react'
|
|||
import { TLUiMenuChild } from '../hooks/menuHelpers'
|
||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||
import { useMenuSchema } from '../hooks/useMenuSchema'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { LanguageMenu } from './LanguageMenu'
|
||||
import { Button } from './primitives/Button'
|
||||
|
@ -35,7 +35,7 @@ function MenuContent() {
|
|||
const msg = useTranslation()
|
||||
const menuSchema = useMenuSchema()
|
||||
const breakpoint = useBreakpoint()
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
function getMenuItem(
|
||||
editor: Editor,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { track, useEditor } from '@tldraw/editor'
|
||||
import { useBreakpoint } from '../hooks/useBreakpoint'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { ActionsMenu } from './ActionsMenu'
|
||||
import { DuplicateButton } from './DuplicateButton'
|
||||
import { Menu } from './Menu'
|
||||
|
@ -13,7 +13,7 @@ export const MenuZone = track(function MenuZone() {
|
|||
const editor = useEditor()
|
||||
|
||||
const breakpoint = useBreakpoint()
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser')
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
||||
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
||||
import { useReadOnly } from '../../hooks/useReadOnly'
|
||||
import { useReadonly } from '../../hooks/useReadonly'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
import { Button } from '../primitives/Button'
|
||||
import { Icon } from '../primitives/Icon'
|
||||
|
@ -36,7 +36,7 @@ export const PageMenu = function PageMenu() {
|
|||
const currentPage = useValue('currentPage', () => editor.currentPage, [editor])
|
||||
|
||||
// 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
|
||||
const maxPageCountReached = useValue(
|
||||
|
|
|
@ -142,7 +142,11 @@ function CommonStylePickerSet({
|
|||
|
||||
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 : (
|
||||
<ButtonPicker
|
||||
title={msg('style-panel.color')}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { GeoShapeGeoStyle, preventDefault, track, useEditor, useValue } from '@t
|
|||
import classNames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
import { useBreakpoint } from '../../hooks/useBreakpoint'
|
||||
import { useReadOnly } from '../../hooks/useReadOnly'
|
||||
import { useReadonly } from '../../hooks/useReadonly'
|
||||
import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema'
|
||||
import { TLUiToolItem } from '../../hooks/useTools'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
|
@ -25,7 +25,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
|
||||
const rMostRecentlyActiveDropdownItem = React.useRef<TLUiToolbarItem | undefined>(undefined)
|
||||
|
||||
const isReadOnly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
const toolbarItems = useToolbarSchema()
|
||||
const laserTool = toolbarItems.find((item) => item.toolItem.id === 'laser')
|
||||
|
||||
|
@ -36,8 +36,8 @@ export const Toolbar = memo(function Toolbar() {
|
|||
editor,
|
||||
])
|
||||
|
||||
const showEditingTools = !isReadOnly
|
||||
const showExtraActions = !(isReadOnly || isHandTool)
|
||||
const showEditingTools = !isReadonly
|
||||
const showExtraActions = !(isReadonly || isHandTool)
|
||||
|
||||
const getTitle = (item: TLUiToolItem) =>
|
||||
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__inner">
|
||||
<div className="tlui-toolbar__left">
|
||||
{!isReadOnly && (
|
||||
{!isReadonly && (
|
||||
<div
|
||||
className={classNames('tlui-toolbar__extras', {
|
||||
'tlui-toolbar__extras__hidden': !showExtraActions,
|
||||
|
@ -144,7 +144,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
/>
|
||||
)
|
||||
})}
|
||||
{isReadOnly && laserTool && (
|
||||
{isReadonly && laserTool && (
|
||||
<ToolbarButton
|
||||
key={laserTool.toolItem.id}
|
||||
item={laserTool.toolItem}
|
||||
|
@ -208,7 +208,7 @@ export const Toolbar = memo(function Toolbar() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{breakpoint < 5 && !isReadOnly && (
|
||||
{breakpoint < 5 && !isReadonly && (
|
||||
<div className="tlui-toolbar__tools">
|
||||
<MobileStylePanel />
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { track, useEditor } from '@tldraw/editor'
|
||||
import { useActions } from '../hooks/useActions'
|
||||
import { useReadOnly } from '../hooks/useReadOnly'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { Button } from './primitives/Button'
|
||||
import { kbdStr } from './primitives/shared'
|
||||
|
@ -11,7 +11,7 @@ export const TrashButton = track(function TrashButton() {
|
|||
const msg = useTranslation()
|
||||
const action = actions['delete']
|
||||
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
|
||||
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 { useEffect } from 'react'
|
||||
import { useActions } from './useActions'
|
||||
import { useReadOnly } from './useReadOnly'
|
||||
import { useReadonly } from './useReadonly'
|
||||
import { useTools } from './useTools'
|
||||
|
||||
const SKIP_KBDS = [
|
||||
|
@ -18,11 +18,14 @@ const SKIP_KBDS = [
|
|||
export function useKeyboardShortcuts() {
|
||||
const editor = useEditor()
|
||||
|
||||
const isReadonly = useReadOnly()
|
||||
const isReadonly = useReadonly()
|
||||
const actions = useActions()
|
||||
const tools = useTools()
|
||||
const isFocused = useValue('is focused', () => editor.instanceState.isFocused, [editor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocused) return
|
||||
|
||||
const container = editor.getContainer()
|
||||
|
||||
hotkeys.setScope(editor.store.id)
|
||||
|
@ -34,9 +37,7 @@ export function useKeyboardShortcuts() {
|
|||
// Add hotkeys for actions and tools.
|
||||
// Except those that in SKIP_KBDS!
|
||||
const areShortcutsDisabled = () =>
|
||||
(editor.instanceState.isFocused && editor.isMenuOpen) ||
|
||||
editor.editingId !== null ||
|
||||
editor.crashingError
|
||||
editor.isMenuOpen || editor.editingId !== null || editor.crashingError
|
||||
|
||||
for (const action of Object.values(actions)) {
|
||||
if (!action.kbd) continue
|
||||
|
@ -51,7 +52,9 @@ export function useKeyboardShortcuts() {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
|
@ -65,7 +68,7 @@ export function useKeyboardShortcuts() {
|
|||
return () => {
|
||||
hotkeys.deleteScope(editor.store.id)
|
||||
}
|
||||
}, [actions, tools, isReadonly, editor])
|
||||
}, [actions, tools, isReadonly, editor, isFocused])
|
||||
}
|
||||
|
||||
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', () => {
|
||||
editor = new TestEditor()
|
||||
editor.updateInstanceState({ isReadOnly: true })
|
||||
editor.updateInstanceState({ isReadonly: true })
|
||||
editor.setCurrentTool('hand')
|
||||
expect(editor.currentToolId).toBe('hand')
|
||||
})
|
||||
|
@ -369,37 +369,42 @@ describe('isFocused', () => {
|
|||
})
|
||||
|
||||
it('becomes true when the container div receives a focus event', () => {
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(false)
|
||||
|
||||
editor.elm.focus()
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(true)
|
||||
})
|
||||
|
||||
it('becomes false when the container div receives a blur event', () => {
|
||||
editor.elm.focus()
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(true)
|
||||
|
||||
editor.elm.blur()
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
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()
|
||||
|
||||
const child = document.createElement('div')
|
||||
editor.elm.appendChild(child)
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(false)
|
||||
|
||||
child.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(true)
|
||||
|
||||
child.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(false)
|
||||
})
|
||||
|
||||
|
@ -413,6 +418,7 @@ describe('isFocused', () => {
|
|||
|
||||
child.dispatchEvent(new FocusEvent('focusout', { bubbles: true }))
|
||||
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(editor.instanceState.isFocused).toBe(false)
|
||||
})
|
||||
|
||||
|
|
|
@ -412,7 +412,7 @@ describe('When in readonly mode', () => {
|
|||
props: { w: 100, h: 100, url: '' },
|
||||
},
|
||||
])
|
||||
editor.updateInstanceState({ isReadOnly: true })
|
||||
editor.updateInstanceState({ isReadonly: true })
|
||||
editor.setCurrentTool('hand')
|
||||
editor.setCurrentTool('select')
|
||||
})
|
||||
|
@ -420,7 +420,7 @@ describe('When in readonly mode', () => {
|
|||
it('Begins editing embed when double clicked', () => {
|
||||
expect(editor.editingId).toBe(null)
|
||||
expect(editor.selectedIds.length).toBe(0)
|
||||
expect(editor.instanceState.isReadOnly).toBe(true)
|
||||
expect(editor.instanceState.isReadonly).toBe(true)
|
||||
|
||||
const shape = editor.getShapeById(ids.embed1)
|
||||
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', () => {
|
||||
expect(editor.editingId).toBe(null)
|
||||
expect(editor.selectedIds.length).toBe(0)
|
||||
expect(editor.instanceState.isReadOnly).toBe(true)
|
||||
expect(editor.instanceState.isReadonly).toBe(true)
|
||||
|
||||
editor.setSelectedIds([ids.embed1])
|
||||
expect(editor.selectedIds.length).toBe(1)
|
||||
|
|
|
@ -279,7 +279,7 @@ describe('creating groups', () => {
|
|||
// │ A │ │ B │ │ C │
|
||||
// └───┘ └───┘ └───┘
|
||||
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.selectAll()
|
||||
expect(editor.selectedIds.length).toBe(3)
|
||||
|
@ -491,7 +491,7 @@ describe('ungrouping shapes', () => {
|
|||
expect(editor.selectedIds.length).toBe(3)
|
||||
editor.groupShapes()
|
||||
expect(editor.selectedIds.length).toBe(1)
|
||||
editor.updateInstanceState({ isReadOnly: true })
|
||||
editor.updateInstanceState({ isReadonly: true })
|
||||
editor.setCurrentTool('hand')
|
||||
|
||||
editor.ungroupShapes()
|
||||
|
|
|
@ -1020,7 +1020,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
|||
// (undocumented)
|
||||
isPenMode: boolean;
|
||||
// (undocumented)
|
||||
isReadOnly: boolean;
|
||||
isReadonly: boolean;
|
||||
// (undocumented)
|
||||
isToolLocked: boolean;
|
||||
// (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 --- */
|
||||
|
||||
for (const migrator of allMigrators) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
|||
isCoarsePointer: boolean
|
||||
openMenus: string[]
|
||||
isChangingStyle: boolean
|
||||
isReadOnly: boolean
|
||||
isReadonly: boolean
|
||||
meta: JsonObject
|
||||
}
|
||||
|
||||
|
@ -87,7 +87,7 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
|
|||
isCoarsePointer: T.boolean,
|
||||
openMenus: T.arrayOf(T.string),
|
||||
isChangingStyle: T.boolean,
|
||||
isReadOnly: T.boolean,
|
||||
isReadonly: T.boolean,
|
||||
meta: T.jsonValue as T.ObjectValidator<JsonObject>,
|
||||
})
|
||||
)
|
||||
|
@ -124,7 +124,7 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
|
|||
isCoarsePointer: false,
|
||||
openMenus: [] as string[],
|
||||
isChangingStyle: false,
|
||||
isReadOnly: false,
|
||||
isReadonly: false,
|
||||
meta: {},
|
||||
})
|
||||
)
|
||||
|
@ -151,11 +151,12 @@ export const instanceVersions = {
|
|||
AddMeta: 17,
|
||||
RemoveCursorColor: 18,
|
||||
AddLonelyProperties: 19,
|
||||
ReadOnlyReadonly: 20,
|
||||
} as const
|
||||
|
||||
/** @public */
|
||||
export const instanceMigrations = defineMigrations({
|
||||
currentVersion: instanceVersions.AddLonelyProperties,
|
||||
currentVersion: instanceVersions.ReadOnlyReadonly,
|
||||
migrators: {
|
||||
[instanceVersions.AddTransparentExportBgs]: {
|
||||
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