kopia lustrzana https://github.com/Tldraw/Tldraw
[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
rodzic
a635145f2a
commit
fb2f515b74
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
Ładowanie…
Reference in New Issue