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') })