[improvement] prevent editing in readonly (#1990)

This PR prevents certain shapes from being edited while in readonly
mode. It adds `ShapeUtil.canEditInReadOnly` to allow developers to opt
in to editing shapes. It's currently applied only to embed shapes.

### Change Type

- [x] `major`

### Test Plan

1. In a readonly mode, try to edit text / sticky notes / arrow labels
via double click / enter. You should not be able to edit them.
2. Try to edit an embed. You should be able to edit it.

### Release Notes

- Prevent editing text shapes in readonly mode.
pull/1999/head
Steve Ruiz 2023-10-03 12:03:01 +01:00 zatwierdzone przez GitHub
rodzic a635145f2a
commit fb2f515b74
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 94 dodań i 46 usunięć

Wyświetl plik

@ -0,0 +1,15 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
export default function ReadOnlyExample() {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="tldraw_example"
onMount={(editor) => {
editor.updateInstanceState({ isReadonly: true })
}}
/>
</div>
)
}

Wyświetl plik

@ -26,6 +26,7 @@ import ExternalContentSourcesExample from './examples/ExternalContentSourcesExam
import HideUiExample from './examples/HideUiExample'
import MultipleExample from './examples/MultipleExample'
import PersistenceExample from './examples/PersistenceExample'
import ReadOnlyExample from './examples/ReadOnlyExample'
import ScrollExample from './examples/ScrollExample'
import ShapeMetaExample from './examples/ShapeMetaExample'
import SnapshotExample from './examples/SnapshotExample/SnapshotExample'
@ -72,6 +73,11 @@ export const allExamples: Example[] = [
path: '/multiple',
element: <MultipleExample />,
},
{
title: 'Readonly Example',
path: '/readonly',
element: <ReadOnlyExample />,
},
{
title: 'Scroll example',
path: '/scroll',

Wyświetl plik

@ -1605,6 +1605,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
canCrop: TLShapeUtilFlag<Shape>;
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag<Shape>;
canEditInReadOnly: TLShapeUtilFlag<Shape>;
canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']): boolean;
canResize: TLShapeUtilFlag<Shape>;
canScroll: TLShapeUtilFlag<Shape>;

Wyświetl plik

@ -115,6 +115,13 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
*/
canResize: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape can be edited in read-only mode.
*
* @public
*/
canEditInReadOnly: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape can be cropped.
*

Wyświetl plik

@ -383,6 +383,8 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented)
canEdit: TLShapeUtilFlag<TLEmbedShape>;
// (undocumented)
canEditInReadOnly: () => boolean;
// (undocumented)
canResize: (shape: TLEmbedShape) => boolean;
// (undocumented)
canUnmount: TLShapeUtilFlag<TLEmbedShape>;

Wyświetl plik

@ -17,6 +17,7 @@ export class Idle extends StateNode {
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
if (info.key === 'Enter') {
if (this.editor.instanceState.isReadonly) return null
const { onlySelectedShape } = this.editor
// If the only selected shape is editable, start editing it
if (

Wyświetl plik

@ -40,6 +40,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
override canResize = (shape: TLEmbedShape) => {
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
}
override canEditInReadOnly = () => true
override getDefaultProps(): TLEmbedShape['props'] {
return {

Wyświetl plik

@ -13,6 +13,8 @@ export class Idle extends StateNode {
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
if (info.key === 'Enter') {
if (this.editor.instanceState.isReadonly) return null
const { onlySelectedShape } = this.editor
// If the only selected shape is editable, start editing it
if (

Wyświetl plik

@ -23,6 +23,7 @@ export class Idle extends StateNode {
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
if (info.key === 'Enter') {
if (this.editor.instanceState.isReadonly) return null
const { onlySelectedShape } = this.editor
// If the only selected shape is editable, start editing it
if (

Wyświetl plik

@ -214,8 +214,6 @@ export class Idle extends StateNode {
}
if (!this.editor.inputs.shiftKey) {
// Create text shape and transition to editing_shape
if (this.editor.instanceState.isReadonly) break
this.handleDoubleClickOnCanvas(info)
}
break
@ -224,9 +222,14 @@ export class Idle extends StateNode {
if (this.editor.instanceState.isReadonly) break
const { onlySelectedShape } = this.editor
if (onlySelectedShape) {
const util = this.editor.getShapeUtil(onlySelectedShape)
if (!this.canInteractWithShapeInReadOnly(onlySelectedShape)) {
return
}
// Test edges for an onDoubleClickEdge handler
if (
info.handle === 'right' ||
@ -416,53 +419,36 @@ export class Idle extends StateNode {
}
override onKeyUp = (info: TLKeyboardEventInfo) => {
if (this.editor.instanceState.isReadonly) {
switch (info.code) {
case 'Enter': {
const { onlySelectedShape } = this.editor
if (onlySelectedShape && this.shouldStartEditingShape()) {
this.startEditingShape(onlySelectedShape, {
...info,
target: 'shape',
shape: onlySelectedShape,
})
return
}
break
switch (info.code) {
case 'Enter': {
const { selectedShapes } = this.editor
// On enter, if every selected shape is a group, then select all of the children of the groups
if (
selectedShapes.every((shape) => this.editor.isShapeOfType<TLGroupShape>(shape, 'group'))
) {
this.editor.setSelectedShapes(
selectedShapes.flatMap((shape) => this.editor.getSortedChildIdsForParent(shape.id))
)
return
}
}
} else {
switch (info.code) {
case 'Enter': {
const { selectedShapes } = this.editor
// On enter, if every selected shape is a group, then select all of the children of the groups
if (
selectedShapes.every((shape) => this.editor.isShapeOfType<TLGroupShape>(shape, 'group'))
) {
this.editor.setSelectedShapes(
selectedShapes.flatMap((shape) => this.editor.getSortedChildIdsForParent(shape.id))
)
return
}
// If the only selected shape is editable, then begin editing it
const { onlySelectedShape } = this.editor
if (onlySelectedShape && this.shouldStartEditingShape(onlySelectedShape)) {
this.startEditingShape(onlySelectedShape, {
...info,
target: 'shape',
shape: onlySelectedShape,
})
return
}
// If the only selected shape is croppable, then begin cropping it
if (getShouldEnterCropMode(this.editor)) {
this.parent.transition('crop', info)
}
break
// If the only selected shape is editable, then begin editing it
const { onlySelectedShape } = this.editor
if (onlySelectedShape && this.shouldStartEditingShape(onlySelectedShape)) {
this.startEditingShape(onlySelectedShape, {
...info,
target: 'shape',
shape: onlySelectedShape,
})
return
}
// If the only selected shape is croppable, then begin cropping it
if (getShouldEnterCropMode(this.editor)) {
this.parent.transition('crop', info)
}
break
}
}
}
@ -470,6 +456,7 @@ export class Idle extends StateNode {
private shouldStartEditingShape(shape: TLShape | null = this.editor.onlySelectedShape): boolean {
if (!shape) return false
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return false
if (!this.canInteractWithShapeInReadOnly(shape)) return false
return this.editor.getShapeUtil(shape).canEdit(shape)
}
@ -483,6 +470,9 @@ export class Idle extends StateNode {
isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
handleDoubleClickOnCanvas(info: TLClickEventInfo) {
// Create text shape and transition to editing_shape
if (this.editor.instanceState.isReadonly) return
this.editor.mark('creating text shape')
const id = createShapeId()
@ -505,6 +495,13 @@ export class Idle extends StateNode {
const shape = this.editor.getShape(id)
if (!shape) return
const util = this.editor.getShapeUtil(shape)
if (this.editor.instanceState.isReadonly) {
if (!util.canEditInReadOnly(shape)) {
return
}
}
this.editor.setEditingShape(id)
this.editor.select(id)
this.parent.transition('editing_shape', info)
@ -545,6 +542,13 @@ export class Idle extends StateNode {
this.editor.nudgeShapes(this.editor.selectedShapeIds, delta.mul(step))
}
private canInteractWithShapeInReadOnly(shape: TLShape) {
if (!this.editor.instanceState.isReadonly) return true
const util = this.editor.getShapeUtil(shape)
if (util.canEditInReadOnly(shape)) return true
return false
}
}
export const MAJOR_NUDGE_FACTOR = 10

Wyświetl plik

@ -145,6 +145,14 @@ export class PointingShape extends StateNode {
this.editor.batch(() => {
this.editor.mark('editing on pointer up')
this.editor.select(selectingShape.id)
const util = this.editor.getShapeUtil(selectingShape)
if (this.editor.instanceState.isReadonly) {
if (!util.canEditInReadOnly(selectingShape)) {
return
}
}
this.editor.setEditingShape(selectingShape.id)
this.editor.setCurrentTool('select.editing_shape')
})