From 343c2f51f3e34956f7e8efdc0fb220fe441e18d4 Mon Sep 17 00:00:00 2001 From: Aaron Dodson Date: Thu, 30 Jan 2025 13:47:36 -0800 Subject: [PATCH] feat: Add support for toggling readonly mode. (#8750) * feat: Add methods for toggling and inspecting the readonly state of a workspace. * refactor: Use the new readonly setters/getters in place of checking the injection options. * fix: Fix bug that allowed dragging blocks from a flyout onto a readonly workspace. * feat: Toggle a `blocklyReadOnly` class when readonly status is changed. * chore: Placate the linter. * chore: Placate the compiler. --- core/block.ts | 6 +++--- core/block_svg.ts | 6 ++++-- core/comments/workspace_comment.ts | 6 +++--- core/dragging/block_drag_strategy.ts | 2 +- core/dragging/comment_drag_strategy.ts | 2 +- core/flyout_base.ts | 2 +- core/gesture.ts | 2 +- core/shortcut_items.ts | 14 +++++++------- core/workspace.ts | 22 ++++++++++++++++++++++ core/workspace_svg.ts | 11 ++++++++++- tests/mocha/keydown_test.js | 2 +- 11 files changed, 54 insertions(+), 21 deletions(-) diff --git a/core/block.ts b/core/block.ts index f5683fcca..b95427bce 100644 --- a/core/block.ts +++ b/core/block.ts @@ -795,7 +795,7 @@ export class Block implements IASTNodeLocation { this.deletable && !this.shadow && !this.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } @@ -828,7 +828,7 @@ export class Block implements IASTNodeLocation { this.movable && !this.shadow && !this.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } @@ -917,7 +917,7 @@ export class Block implements IASTNodeLocation { */ isEditable(): boolean { return ( - this.editable && !this.isDeadOrDying() && !this.workspace.options.readOnly + this.editable && !this.isDeadOrDying() && !this.workspace.isReadOnly() ); } diff --git a/core/block_svg.ts b/core/block_svg.ts index 1f30852c5..a33a21a85 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -231,7 +231,7 @@ export class BlockSvg this.applyColour(); this.pathObject.updateMovable(this.isMovable() || this.isInFlyout); const svg = this.getSvgRoot(); - if (!this.workspace.options.readOnly && svg) { + if (svg) { browserEvents.conditionalBind(svg, 'pointerdown', this, this.onMouseDown); } @@ -585,6 +585,8 @@ export class BlockSvg * @param e Pointer down event. */ private onMouseDown(e: PointerEvent) { + if (this.workspace.isReadOnly()) return; + const gesture = this.workspace.getGesture(e); if (gesture) { gesture.handleBlockStart(e, this); @@ -612,7 +614,7 @@ export class BlockSvg protected generateContextMenu(): Array< ContextMenuOption | LegacyContextMenuOption > | null { - if (this.workspace.options.readOnly || !this.contextMenu) { + if (this.workspace.isReadOnly() || !this.contextMenu) { return null; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( diff --git a/core/comments/workspace_comment.ts b/core/comments/workspace_comment.ts index f21ece8a1..190efd64d 100644 --- a/core/comments/workspace_comment.ts +++ b/core/comments/workspace_comment.ts @@ -144,7 +144,7 @@ export class WorkspaceComment { * workspace is read-only. */ isEditable(): boolean { - return this.isOwnEditable() && !this.workspace.options.readOnly; + return this.isOwnEditable() && !this.workspace.isReadOnly(); } /** @@ -165,7 +165,7 @@ export class WorkspaceComment { * workspace is read-only. */ isMovable() { - return this.isOwnMovable() && !this.workspace.options.readOnly; + return this.isOwnMovable() && !this.workspace.isReadOnly(); } /** @@ -189,7 +189,7 @@ export class WorkspaceComment { return ( this.isOwnDeletable() && !this.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } diff --git a/core/dragging/block_drag_strategy.ts b/core/dragging/block_drag_strategy.ts index c9a1ea0ab..07b9bca5b 100644 --- a/core/dragging/block_drag_strategy.ts +++ b/core/dragging/block_drag_strategy.ts @@ -78,7 +78,7 @@ export class BlockDragStrategy implements IDragStrategy { return ( this.block.isOwnMovable() && !this.block.isDeadOrDying() && - !this.workspace.options.readOnly && + !this.workspace.isReadOnly() && // We never drag blocks in the flyout, only create new blocks that are // dragged. !this.block.isInFlyout diff --git a/core/dragging/comment_drag_strategy.ts b/core/dragging/comment_drag_strategy.ts index dd8b10fc2..9e051d5bc 100644 --- a/core/dragging/comment_drag_strategy.ts +++ b/core/dragging/comment_drag_strategy.ts @@ -29,7 +29,7 @@ export class CommentDragStrategy implements IDragStrategy { return ( this.comment.isOwnMovable() && !this.comment.isDeadOrDying() && - !this.workspace.options.readOnly + !this.workspace.isReadOnly() ); } diff --git a/core/flyout_base.ts b/core/flyout_base.ts index 817076b97..5b2e91b7c 100644 --- a/core/flyout_base.ts +++ b/core/flyout_base.ts @@ -786,7 +786,7 @@ export abstract class Flyout * @internal */ isBlockCreatable(block: BlockSvg): boolean { - return block.isEnabled(); + return block.isEnabled() && !this.getTargetWorkspace().isReadOnly(); } /** diff --git a/core/gesture.ts b/core/gesture.ts index 0b65299e5..fc23ba7ca 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -894,7 +894,7 @@ export class Gesture { 'Cannot do a block click because the target block is ' + 'undefined', ); } - if (this.targetBlock.isEnabled()) { + if (this.flyout.isBlockCreatable(this.targetBlock)) { if (!eventUtils.getGroup()) { eventUtils.setGroup(true); } diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index 0db28a51a..0793e6213 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -40,7 +40,7 @@ export function registerEscape() { const escapeAction: KeyboardShortcut = { name: names.ESCAPE, preconditionFn(workspace) { - return !workspace.options.readOnly; + return !workspace.isReadOnly(); }, callback(workspace) { // AnyDuringMigration because: Property 'hideChaff' does not exist on @@ -62,7 +62,7 @@ export function registerDelete() { preconditionFn(workspace) { const selected = common.getSelected(); return ( - !workspace.options.readOnly && + !workspace.isReadOnly() && selected != null && isDeletable(selected) && selected.isDeletable() && @@ -113,7 +113,7 @@ export function registerCopy() { preconditionFn(workspace) { const selected = common.getSelected(); return ( - !workspace.options.readOnly && + !workspace.isReadOnly() && !Gesture.inProgress() && selected != null && isDeletable(selected) && @@ -164,7 +164,7 @@ export function registerCut() { preconditionFn(workspace) { const selected = common.getSelected(); return ( - !workspace.options.readOnly && + !workspace.isReadOnly() && !Gesture.inProgress() && selected != null && isDeletable(selected) && @@ -221,7 +221,7 @@ export function registerPaste() { const pasteShortcut: KeyboardShortcut = { name: names.PASTE, preconditionFn(workspace) { - return !workspace.options.readOnly && !Gesture.inProgress(); + return !workspace.isReadOnly() && !Gesture.inProgress(); }, callback() { if (!copyData || !copyWorkspace) return false; @@ -269,7 +269,7 @@ export function registerUndo() { const undoShortcut: KeyboardShortcut = { name: names.UNDO, preconditionFn(workspace) { - return !workspace.options.readOnly && !Gesture.inProgress(); + return !workspace.isReadOnly() && !Gesture.inProgress(); }, callback(workspace, e) { // 'z' for undo 'Z' is for redo. @@ -308,7 +308,7 @@ export function registerRedo() { const redoShortcut: KeyboardShortcut = { name: names.REDO, preconditionFn(workspace) { - return !Gesture.inProgress() && !workspace.options.readOnly; + return !Gesture.inProgress() && !workspace.isReadOnly(); }, callback(workspace, e) { // 'z' for undo 'Z' is for redo. diff --git a/core/workspace.ts b/core/workspace.ts index 265420ec0..30238b91e 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -114,6 +114,7 @@ export class Workspace implements IASTNodeLocation { private readonly typedBlocksDB = new Map(); private variableMap: IVariableMap>; private procedureMap: IProcedureMap = new ObservableProcedureMap(); + private readOnly = false; /** * Blocks in the flyout can refer to variables that don't exist in the main @@ -153,6 +154,8 @@ export class Workspace implements IASTNodeLocation { */ const VariableMap = this.getVariableMapClass(); this.variableMap = new VariableMap(this); + + this.setIsReadOnly(this.options.readOnly); } /** @@ -947,4 +950,23 @@ export class Workspace implements IASTNodeLocation { } return VariableMap; } + + /** + * Returns whether or not this workspace is in readonly mode. + * + * @returns True if the workspace is readonly, otherwise false. + */ + isReadOnly(): boolean { + return this.readOnly; + } + + /** + * Sets whether or not this workspace is in readonly mode. + * + * @param readOnly True to make the workspace readonly, otherwise false. + */ + setIsReadOnly(readOnly: boolean) { + this.readOnly = readOnly; + this.options.readOnly = readOnly; + } } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index e9aac5b9d..78506115e 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1703,7 +1703,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ showContextMenu(e: PointerEvent) { - if (this.options.readOnly || this.isFlyout) { + if (this.isReadOnly() || this.isFlyout) { return; } const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( @@ -2532,6 +2532,15 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { dom.removeClass(this.injectionDiv, className); } } + + override setIsReadOnly(readOnly: boolean) { + super.setIsReadOnly(readOnly); + if (readOnly) { + this.addClass('blocklyReadOnly'); + } else { + this.removeClass('blocklyReadOnly'); + } + } } /** diff --git a/tests/mocha/keydown_test.js b/tests/mocha/keydown_test.js index 0b72a7fee..82293f224 100644 --- a/tests/mocha/keydown_test.js +++ b/tests/mocha/keydown_test.js @@ -42,7 +42,7 @@ suite('Key Down', function () { function runReadOnlyTest(keyEvent, opt_name) { const name = opt_name ? opt_name : 'Not called when readOnly is true'; test(name, function () { - this.workspace.options.readOnly = true; + this.workspace.setIsReadOnly(true); this.injectionDiv.dispatchEvent(keyEvent); sinon.assert.notCalled(this.hideChaffSpy); });