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.
This commit is contained in:
Aaron Dodson
2025-01-30 13:47:36 -08:00
committed by GitHub
parent c88ebf1ede
commit 343c2f51f3
11 changed files with 54 additions and 21 deletions

View File

@@ -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()
);
}

View File

@@ -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(

View File

@@ -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()
);
}

View File

@@ -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

View File

@@ -29,7 +29,7 @@ export class CommentDragStrategy implements IDragStrategy {
return (
this.comment.isOwnMovable() &&
!this.comment.isDeadOrDying() &&
!this.workspace.options.readOnly
!this.workspace.isReadOnly()
);
}

View File

@@ -786,7 +786,7 @@ export abstract class Flyout
* @internal
*/
isBlockCreatable(block: BlockSvg): boolean {
return block.isEnabled();
return block.isEnabled() && !this.getTargetWorkspace().isReadOnly();
}
/**

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -114,6 +114,7 @@ export class Workspace implements IASTNodeLocation {
private readonly typedBlocksDB = new Map<string, Block[]>();
private variableMap: IVariableMap<IVariableModel<IVariableState>>;
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;
}
}

View File

@@ -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');
}
}
}
/**

View File

@@ -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);
});