diff --git a/package-lock.json b/package-lock.json index 0d9c99f8..91d4b773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@blockly/keyboard-experiment", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@blockly/keyboard-experiment", - "version": "0.0.4", + "version": "0.0.5", "license": "Apache-2.0", "devDependencies": { "@blockly/dev-scripts": "^4.0.1", @@ -16,7 +16,7 @@ "@types/p5": "^1.7.6", "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", - "blockly": "^11.2.1", + "blockly": "12.0.0-beta.2", "eslint": "^8.49.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", @@ -29,7 +29,7 @@ "typescript": "^5.4.5" }, "peerDependencies": { - "blockly": "^11.1.0" + "blockly": "^12.0.0-beta.2" } }, "node_modules/@asamuzakjp/css-color": { @@ -2187,9 +2187,9 @@ } }, "node_modules/blockly": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/blockly/-/blockly-11.2.1.tgz", - "integrity": "sha512-20sCwSwX2Z6UxR/er0B5y6wRFukuIdvOjc7jMuIwyCO/yT35+UbAqYueMga3JFA9NoWPwQc+3s6/XnLkyceAww==", + "version": "12.0.0-beta.2", + "resolved": "https://registry.npmjs.org/blockly/-/blockly-12.0.0-beta.2.tgz", + "integrity": "sha512-I3KgGfw6E/S6dTcPrzQLxA+/mu3PqoDLEV34tdWME6twURlSYObMZPLoZXOc1h35tQsvorJMUKDK6V+qdz6eww==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index a6e160a1..dcb15cd9 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@types/p5": "^1.7.6", "@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/parser": "^6.7.2", - "blockly": "^11.2.1", + "blockly": "12.0.0-beta.2", "eslint": "^8.49.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^9.0.0", @@ -61,7 +61,12 @@ "typescript": "^5.4.5" }, "peerDependencies": { - "blockly": "^11.1.0" + "blockly": "^12.0.0-beta.2" + }, + "overrides": { + "@blockly/field-colour": { + "blockly": "^12.0.0-beta.2" + } }, "publishConfig": { "access": "public", diff --git a/src/actions/action_menu.ts b/src/actions/action_menu.ts index cb98a576..5eab1325 100644 --- a/src/actions/action_menu.ts +++ b/src/actions/action_menu.ts @@ -111,6 +111,7 @@ export class ActionMenu { const cursor = workspace.getCursor(); if (!cursor) throw new Error('workspace has no cursor'); const node = cursor.getCurNode(); + if (!node) throw new Error('No node is currently selected'); const nodeType = node.getType(); switch (nodeType) { case ASTNode.types.BLOCK: @@ -195,16 +196,16 @@ export class ActionMenu { connection, } as unknown as ContextMenuRegistry.Scope; for (const option of possibleOptions) { - const precondition = option.preconditionFn(scope); + const precondition = option.preconditionFn?.(scope); if (precondition === 'hidden') continue; const displayText = - typeof option.displayText === 'function' + (typeof option.displayText === 'function' ? option.displayText(scope) - : option.displayText; + : option.displayText) ?? ''; menuOptions.push({ text: displayText, enabled: precondition === 'enabled', - callback: option.callback, + callback: option.callback!, scope, weight: option.weight, }); diff --git a/src/actions/arrow_navigation.ts b/src/actions/arrow_navigation.ts index db50aad7..f63dd686 100644 --- a/src/actions/arrow_navigation.ts +++ b/src/actions/arrow_navigation.ts @@ -40,7 +40,7 @@ export class ArrowNavigation { return false; } const curNode = cursor.getCurNode(); - if (curNode.getType() === ASTNode.types.FIELD) { + if (curNode?.getType() === ASTNode.types.FIELD) { return (curNode.getLocation() as Field).onShortcut(shortcut); } return false; diff --git a/src/actions/clipboard.ts b/src/actions/clipboard.ts index 4788cf14..46e8733d 100644 --- a/src/actions/clipboard.ts +++ b/src/actions/clipboard.ts @@ -169,7 +169,9 @@ export class Clipboard { private cutCallback(workspace: WorkspaceSvg) { const cursor = workspace.getCursor(); if (!cursor) throw new TypeError('no cursor'); - const sourceBlock = cursor.getCurNode().getSourceBlock() as BlockSvg | null; + const sourceBlock = cursor + .getCurNode() + ?.getSourceBlock() as BlockSvg | null; if (!sourceBlock) throw new TypeError('no source block'); this.copyData = sourceBlock.toCopyData(); this.copyWorkspace = sourceBlock.workspace; @@ -274,7 +276,9 @@ export class Clipboard { const sourceBlock = activeWorkspace ?.getCursor() ?.getCurNode() - .getSourceBlock() as BlockSvg; + ?.getSourceBlock() as BlockSvg; + if (!sourceBlock) return false; + this.copyData = sourceBlock.toCopyData(); this.copyWorkspace = sourceBlock.workspace; const copied = !!this.copyData; diff --git a/src/actions/delete.ts b/src/actions/delete.ts index 7b6f2a9d..33498444 100644 --- a/src/actions/delete.ts +++ b/src/actions/delete.ts @@ -113,8 +113,8 @@ export class DeleteAction { // Run the original precondition code, from the context menu option. // If the item would be hidden or disabled, respect it. const originalPreconditionResult = - this.oldContextMenuItem!.preconditionFn(scope); - if (!ws || originalPreconditionResult != 'enabled') { + this.oldContextMenuItem!.preconditionFn?.(scope) ?? 'enabled'; + if (!ws || originalPreconditionResult !== 'enabled') { return originalPreconditionResult; } @@ -150,7 +150,7 @@ export class DeleteAction { private deletePrecondition(workspace: WorkspaceSvg) { if (!this.canCurrentlyEdit(workspace)) return false; - const sourceBlock = workspace.getCursor()?.getCurNode().getSourceBlock(); + const sourceBlock = workspace.getCursor()?.getCurNode()?.getSourceBlock(); return !!sourceBlock?.isDeletable(); } @@ -169,7 +169,10 @@ export class DeleteAction { const cursor = workspace.getCursor(); if (!cursor) return false; - const sourceBlock = cursor.getCurNode().getSourceBlock() as BlockSvg; + const sourceBlock = cursor + .getCurNode() + ?.getSourceBlock() as BlockSvg | null; + if (!sourceBlock) return false; // Delete or backspace. // There is an event if this is triggered from a keyboard shortcut, // but not if it's triggered from a context menu. diff --git a/src/actions/enter.ts b/src/actions/enter.ts index 82223c63..c168a627 100644 --- a/src/actions/enter.ts +++ b/src/actions/enter.ts @@ -64,7 +64,7 @@ export class EnterAction { return false; } curNode = flyoutCursor.getCurNode(); - nodeType = curNode.getType(); + nodeType = curNode?.getType(); switch (nodeType) { case ASTNode.types.STACK: @@ -157,7 +157,8 @@ export class EnterAction { const button = this.navigation .getFlyoutCursor(workspace)! .getCurNode() - .getLocation() as FlyoutButton; + ?.getLocation() as FlyoutButton | undefined; + if (!button) return; const buttonCallback = (workspace as any).flyoutButtonCallbacks.get( (button as any).callbackKey, ); @@ -209,8 +210,8 @@ export class EnterAction { const curBlock = this.navigation .getFlyoutCursor(workspace)! .getCurNode() - .getLocation() as BlockSvg; - if (!curBlock.isEnabled()) { + ?.getLocation() as BlockSvg | undefined; + if (!curBlock?.isEnabled()) { console.warn("Can't insert a disabled block."); return null; } diff --git a/src/actions/ws_movement.ts b/src/actions/ws_movement.ts index 436c1186..25700c9d 100644 --- a/src/actions/ws_movement.ts +++ b/src/actions/ws_movement.ts @@ -108,7 +108,7 @@ export class WorkspaceMovement { const cursor = workspace.getCursor(); if (!cursor) return false; const curNode = cursor?.getCurNode(); - if (curNode.getType() !== ASTNode.types.WORKSPACE) return false; + if (!curNode || curNode.getType() !== ASTNode.types.WORKSPACE) return false; const wsCoord = curNode.getWsCoordinate(); const newX = xDirection * this.WS_MOVE_DISTANCE + wsCoord.x; diff --git a/src/line_cursor.ts b/src/line_cursor.ts index 4eebf195..5bc2848c 100644 --- a/src/line_cursor.ts +++ b/src/line_cursor.ts @@ -79,7 +79,8 @@ export class LineCursor extends Marker { const markerManager = this.workspace.getMarkerManager(); this.oldCursor = markerManager.getCursor(); markerManager.setCursor(this); - if (this.oldCursor) this.setCurNode(this.oldCursor.getCurNode()); + const oldCursorNode = this.oldCursor?.getCurNode(); + if (oldCursorNode) this.setCurNode(oldCursorNode); this.workspace.addChangeListener(this.selectListener); this.installed = true; } @@ -423,7 +424,7 @@ export class LineCursor extends Marker { preDelete(deletedBlock: Blockly.Block) { const curNode = this.getCurNode(); - const nodes: Blockly.ASTNode[] = [curNode]; + const nodes: Blockly.ASTNode[] = curNode ? [curNode] : []; // The connection to which the deleted block is attached. const parentConnection = deletedBlock.previousConnection?.targetConnection ?? @@ -446,7 +447,7 @@ export class LineCursor extends Marker { } // A location on the workspace beneath the deleted block. // Move to the workspace. - const curBlock = curNode.getSourceBlock(); + const curBlock = curNode?.getSourceBlock(); if (curBlock) { const workspaceNode = Blockly.ASTNode.createWorkspaceNode( this.workspace, @@ -474,6 +475,40 @@ export class LineCursor extends Marker { throw new Error('no valid nodes in this.potentialNodes'); } + /** + * Sets the object in charge of drawing the marker. + * + * We want to customize drawing, so rather than directly setting the given + * object, we instead set a wrapper proxy object that passes through all + * method calls and property accesses except for draw(), which it delegates + * to the drawMarker() method in this class. + * + * @param drawer The object ~in charge of drawing the marker. + */ + override setDrawer(drawer: Blockly.blockRendering.MarkerSvg) { + const altDraw = function ( + this: LineCursor, + oldNode: ASTNode | null, + curNode: ASTNode | null, + ) { + // Pass the unproxied, raw drawer object so that drawMarker can call its + // `draw()` method without triggering infinite recursion. + this.drawMarker(oldNode, curNode, drawer); + }.bind(this); + + super.setDrawer( + new Proxy(drawer, { + get(target: typeof drawer, prop: keyof typeof drawer) { + if (prop === 'draw') { + return altDraw; + } + + return target[prop]; + }, + }), + ); + } + /** * Set the location of the cursor and draw it. * @@ -482,7 +517,7 @@ export class LineCursor extends Marker { * * @param newNode The new location of the cursor. */ - override setCurNode(newNode: ASTNode, selectionInSync = false) { + override setCurNode(newNode: ASTNode | null, selectionInSync = false) { if (newNode?.getLocation() === this.getCurNode()?.getLocation()) { return; } @@ -505,18 +540,8 @@ export class LineCursor extends Marker { } } - const oldNode = super.getCurNode(); - // Kludge: we can't set this.curNode directly, so we have to call - // super.setCurNode(...) to do it for us - but that would call - // this.drawer.draw(...), so prevent that by temporarily setting - // this.drawer to null (which we also can't do directly!) - const drawer = this.getDrawer(); - this.setDrawer(null as any); // Cast required since param is not nullable. super.setCurNode(newNode); - this.setDrawer(drawer); - // Draw this marker the way we want to. - this.drawMarker(oldNode, newNode); // Try to scroll cursor into view. if (newNode?.getType() === ASTNode.types.BLOCK) { const block = newNode.getLocation() as Blockly.BlockSvg; @@ -527,21 +552,6 @@ export class LineCursor extends Marker { } } - /** - * Redraw the current marker. - * - * Overrides normal Marker drawing logic to use this.drawMarker() - * instead of this.drawer.draw() directly. - * - * This hooks the method used by the renderer to draw the marker, - * preventing the marker drawer from showing a marker if we don't - * want it to. - */ - override draw() { - const curNode = super.getCurNode(); - this.drawMarker(curNode, curNode); - } - /** * Draw this cursor's marker. * @@ -566,7 +576,11 @@ export class LineCursor extends Marker { * @param oldNode The previous node. * @param curNode The current node. */ - private drawMarker(oldNode: ASTNode, curNode: ASTNode) { + private drawMarker( + oldNode: ASTNode | null, + curNode: ASTNode | null, + realDrawer: Blockly.blockRendering.MarkerSvg, + ) { // If old node was a block, unselect it or remove fake selection. if (oldNode?.getType() === ASTNode.types.BLOCK) { const block = oldNode.getLocation() as Blockly.BlockSvg; @@ -577,26 +591,26 @@ export class LineCursor extends Marker { } } - if (this.isZelos && this.isValueInputConnection(oldNode)) { + if (this.isZelos && oldNode && this.isValueInputConnection(oldNode)) { this.hideAtInput(oldNode); } const curNodeType = curNode?.getType(); const isZelosInputConnection = - this.isZelos && this.isValueInputConnection(curNode); + this.isZelos && curNode && this.isValueInputConnection(curNode); // If drawing can't be handled locally, just use the drawer. if (curNodeType !== ASTNode.types.BLOCK && !isZelosInputConnection) { - this.getDrawer()?.draw(oldNode, curNode); + realDrawer.draw(oldNode, curNode); return; } // Hide any visible marker SVG and instead do some manual rendering. - super.hide(); // Calls this.drawer?.hide(). + realDrawer.hide(); if (isZelosInputConnection) { this.showAtInput(curNode); - } else if (curNodeType === ASTNode.types.BLOCK) { + } else if (curNode && curNodeType === ASTNode.types.BLOCK) { const block = curNode.getLocation() as Blockly.BlockSvg; if (!block.isShadow()) { // Selection should already be in sync. @@ -607,7 +621,7 @@ export class LineCursor extends Marker { // Call MarkerSvg.prototype.fireMarkerEvent like // MarkerSvg.prototype.draw would (even though it's private). - (this.getDrawer() as any)?.fireMarkerEvent?.(oldNode, curNode); + (realDrawer as any)?.fireMarkerEvent?.(oldNode, curNode); } /** diff --git a/src/navigation.ts b/src/navigation.ts index 5d97409a..5f879aae 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -338,14 +338,14 @@ export class Navigation { if ( !cursor || !cursor.getCurNode() || - !cursor.getCurNode().getSourceBlock() + !cursor.getCurNode()?.getSourceBlock() ) { return; } const curNode = cursor.getCurNode(); - const sourceBlock = curNode.getSourceBlock()!; - if (sourceBlock.id === deletedBlockId || ids.includes(sourceBlock.id)) { + const sourceBlock = curNode?.getSourceBlock()!; + if (sourceBlock?.id === deletedBlockId || ids.includes(sourceBlock?.id)) { cursor.setCurNode( Blockly.ASTNode.createWorkspaceNode( workspace, @@ -423,11 +423,12 @@ export class Navigation { this.setState(workspace, Constants.STATE.NOWHERE); const cursor = workspace.getCursor(); if (cursor) { - if (cursor.getCurNode()) { - this.passiveFocusIndicator.show(cursor.getCurNode()); + const curNode = cursor.getCurNode(); + if (curNode) { + this.passiveFocusIndicator.show(curNode); } // It's initially null so this is a valid state despite the types. - cursor.setCurNode(null as never); + cursor.setCurNode(null); } } @@ -544,20 +545,21 @@ export class Navigation { const flyoutCursor = this.getFlyoutCursor(workspace); if (!flyoutCursor) return; - if ( - flyoutCursor.getCurNode() && - !this.isFlyoutItemDisposed(flyoutCursor.getCurNode()) - ) - return; + const curNode = flyoutCursor.getCurNode(); + if (curNode && !this.isFlyoutItemDisposed(curNode)) return; const flyoutContents = flyout.getContents(); const firstFlyoutItem = flyoutContents[0]; if (!firstFlyoutItem) return; - if (firstFlyoutItem.button) { - const astNode = Blockly.ASTNode.createButtonNode(firstFlyoutItem.button); + if (firstFlyoutItem.getElement() instanceof Blockly.FlyoutButton) { + const astNode = Blockly.ASTNode.createButtonNode( + firstFlyoutItem.getElement() as Blockly.FlyoutButton, + ); flyoutCursor.setCurNode(astNode!); - } else if (firstFlyoutItem.block) { - const astNode = Blockly.ASTNode.createStackNode(firstFlyoutItem.block); + } else if (firstFlyoutItem.getElement() instanceof Blockly.BlockSvg) { + const astNode = Blockly.ASTNode.createStackNode( + firstFlyoutItem.getElement() as Blockly.BlockSvg, + ); flyoutCursor.setCurNode(astNode!); } } diff --git a/src/workspace_utilities.ts b/src/workspace_utilities.ts index 4e1fd298..e9fb450a 100644 --- a/src/workspace_utilities.ts +++ b/src/workspace_utilities.ts @@ -91,7 +91,7 @@ export function getToolboxElement( const toolbox = workspace.getToolbox(); if (toolbox instanceof Blockly.Toolbox) { return toolbox.HtmlDiv?.querySelector( - '.blocklyToolboxContents', + '.blocklyToolboxCategoryGroup', ) as HTMLElement | null; } return null;