diff --git a/apps/examples/src/examples/ReadOnlyExample.tsx b/apps/examples/src/examples/ReadOnlyExample.tsx
new file mode 100644
index 000000000..446daacd2
--- /dev/null
+++ b/apps/examples/src/examples/ReadOnlyExample.tsx
@@ -0,0 +1,15 @@
+import { Tldraw } from '@tldraw/tldraw'
+import '@tldraw/tldraw/tldraw.css'
+
+export default function ReadOnlyExample() {
+ return (
+
+ {
+ editor.updateInstanceState({ isReadonly: true })
+ }}
+ />
+
+ )
+}
diff --git a/apps/examples/src/index.tsx b/apps/examples/src/index.tsx
index 4b4a261c8..22c3027f6 100644
--- a/apps/examples/src/index.tsx
+++ b/apps/examples/src/index.tsx
@@ -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: ,
},
+ {
+ title: 'Readonly Example',
+ path: '/readonly',
+ element: ,
+ },
{
title: 'Scroll example',
path: '/scroll',
diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md
index 772267a32..aa0a7a45d 100644
--- a/packages/editor/api-report.md
+++ b/packages/editor/api-report.md
@@ -1605,6 +1605,7 @@ export abstract class ShapeUtil {
canCrop: TLShapeUtilFlag;
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag;
+ canEditInReadOnly: TLShapeUtilFlag;
canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']): boolean;
canResize: TLShapeUtilFlag;
canScroll: TLShapeUtilFlag;
diff --git a/packages/editor/src/lib/editor/shapes/ShapeUtil.ts b/packages/editor/src/lib/editor/shapes/ShapeUtil.ts
index 9edd1baaf..973c07709 100644
--- a/packages/editor/src/lib/editor/shapes/ShapeUtil.ts
+++ b/packages/editor/src/lib/editor/shapes/ShapeUtil.ts
@@ -115,6 +115,13 @@ export abstract class ShapeUtil {
*/
canResize: TLShapeUtilFlag = () => true
+ /**
+ * Whether the shape can be edited in read-only mode.
+ *
+ * @public
+ */
+ canEditInReadOnly: TLShapeUtilFlag = () => false
+
/**
* Whether the shape can be cropped.
*
diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md
index 8b0e5de00..1932574ca 100644
--- a/packages/tldraw/api-report.md
+++ b/packages/tldraw/api-report.md
@@ -383,6 +383,8 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil {
// (undocumented)
canEdit: TLShapeUtilFlag;
// (undocumented)
+ canEditInReadOnly: () => boolean;
+ // (undocumented)
canResize: (shape: TLEmbedShape) => boolean;
// (undocumented)
canUnmount: TLShapeUtilFlag;
diff --git a/packages/tldraw/src/lib/shapes/arrow/toolStates/Idle.ts b/packages/tldraw/src/lib/shapes/arrow/toolStates/Idle.ts
index ba3e83681..fc20d8065 100644
--- a/packages/tldraw/src/lib/shapes/arrow/toolStates/Idle.ts
+++ b/packages/tldraw/src/lib/shapes/arrow/toolStates/Idle.ts
@@ -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 (
diff --git a/packages/tldraw/src/lib/shapes/embed/EmbedShapeUtil.tsx b/packages/tldraw/src/lib/shapes/embed/EmbedShapeUtil.tsx
index 53b07cacc..2151b78de 100644
--- a/packages/tldraw/src/lib/shapes/embed/EmbedShapeUtil.tsx
+++ b/packages/tldraw/src/lib/shapes/embed/EmbedShapeUtil.tsx
@@ -40,6 +40,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil {
override canResize = (shape: TLEmbedShape) => {
return !!getEmbedInfo(shape.props.url)?.definition?.doesResize
}
+ override canEditInReadOnly = () => true
override getDefaultProps(): TLEmbedShape['props'] {
return {
diff --git a/packages/tldraw/src/lib/shapes/geo/toolStates/Idle.ts b/packages/tldraw/src/lib/shapes/geo/toolStates/Idle.ts
index 81faaaa47..05791859b 100644
--- a/packages/tldraw/src/lib/shapes/geo/toolStates/Idle.ts
+++ b/packages/tldraw/src/lib/shapes/geo/toolStates/Idle.ts
@@ -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 (
diff --git a/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts b/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts
index 8eb4e55b1..943e86c08 100644
--- a/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts
+++ b/packages/tldraw/src/lib/shapes/text/toolStates/Idle.ts
@@ -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 (
diff --git a/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts b/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts
index 060804f31..3753e99e3 100644
--- a/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts
+++ b/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts
@@ -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(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(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
diff --git a/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts b/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts
index 855798703..6a5c4b6e2 100644
--- a/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts
+++ b/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts
@@ -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')
})